From bc98ab644c92efec8ff744f2a09634b14df8a5ce Mon Sep 17 00:00:00 2001 From: Sallvainian Date: Mon, 28 Jul 2025 19:25:32 -0400 Subject: [PATCH 01/14] Add Windows compatibility and fix notification settings synchronization - Add cross-platform path handling utilities - Update build scripts to use shx for Windows compatibility - Fix TypeScript ref type issues in input components - Enhance worktree deletion to handle Windows file locks - Make keyboard shortcut display platform-aware (Cmd vs Ctrl) - Fix notification toggle synchronization across components - Add proper settings reload when notification settings change This ensures Crystal works seamlessly on Windows while maintaining full compatibility with macOS and Linux. Fixes notification settings not being properly applied when toggled off. --- NOTICES | 538 +++++++++++++++++- .../src/components/FilePathAutocomplete.tsx | 2 +- frontend/src/components/Settings.tsx | 8 +- .../src/components/session/SessionInput.tsx | 8 +- .../session/SessionInputWithImages.tsx | 10 +- frontend/src/hooks/useNotifications.ts | 52 +- frontend/src/utils/pathUtils.ts | 88 +++ frontend/src/utils/platform.ts | 33 ++ main/package.json | 3 +- main/src/database/database.ts | 4 +- main/src/ipc/session.ts | 28 + main/src/services/worktreeManager.ts | 89 ++- pnpm-lock.yaml | 38 ++ 13 files changed, 866 insertions(+), 35 deletions(-) create mode 100644 frontend/src/utils/pathUtils.ts create mode 100644 frontend/src/utils/platform.ts diff --git a/NOTICES b/NOTICES index 5c83c854..0e50eddb 100644 --- a/NOTICES +++ b/NOTICES @@ -15,6 +15,7 @@ The following packages are licensed under the MIT license: - @antfu/install-pkg (1.1.0) - Anthony Fu - @antfu/utils (8.1.1) - Anthony Fu - @anthropic-ai/sdk (0.53.0) - Anthropic + - @anthropic-ai/sdk (0.57.0) - Anthropic - @babel/code-frame (7.27.1) - The Babel Team (https://babel.dev/team) - @babel/compat-data (7.28.0) - The Babel Team (https://babel.dev/team) - @babel/core (7.28.0) - The Babel Team (https://babel.dev/team) @@ -27,17 +28,26 @@ The following packages are licensed under the MIT license: - @babel/helper-validator-identifier (7.27.1) - The Babel Team (https://babel.dev/team) - @babel/helper-validator-option (7.27.1) - The Babel Team (https://babel.dev/team) - @babel/helpers (7.27.6) - The Babel Team (https://babel.dev/team) + - @babel/helpers (7.28.2) - The Babel Team (https://babel.dev/team) - @babel/parser (7.28.0) - The Babel Team (https://babel.dev/team) - @babel/runtime (7.27.6) - The Babel Team (https://babel.dev/team) + - @babel/runtime (7.28.2) - The Babel Team (https://babel.dev/team) - @babel/template (7.27.2) - The Babel Team (https://babel.dev/team) - @babel/traverse (7.28.0) - The Babel Team (https://babel.dev/team) - @babel/types (7.28.1) - The Babel Team (https://babel.dev/team) + - @babel/types (7.28.2) - The Babel Team (https://babel.dev/team) + - @bcoe/v8-coverage (0.2.3) - Charles Samborski (https://demurgos.net) - @braintree/sanitize-url (7.1.1) - @develar/schema-utils (2.6.5) - webpack Contrib (https://github.com/webpack-contrib) + - @electron/asar (3.2.18) - @electron/asar (3.4.1) + - @electron/fuses (1.8.0) - Electron Community - @electron/get (2.0.3) - Samuel Attard + - @electron/node-gyp (10.2.0-electron.1) - Nathan Rajlich (http://tootallnate.net) - @electron/notarize (2.5.0) - Samuel Attard - @electron/rebuild (3.6.1) - Paul Betts + - @electron/rebuild (3.7.0) + - @electron/rebuild (4.0.1) - @electron/universal (2.0.1) - Samuel Attard - @emotion/babel-plugin (11.13.5) - Kye Hohenberger - @emotion/cache (11.14.0) @@ -49,16 +59,71 @@ The following packages are licensed under the MIT license: - @emotion/unitless (0.10.0) - @emotion/utils (1.4.2) - @emotion/weak-memoize (0.4.0) +<<<<<<< HEAD - @esbuild/darwin-arm64 (0.25.7) +======= + - @esbuild/aix-ppc64 (0.21.5) + - @esbuild/aix-ppc64 (0.25.8) + - @esbuild/android-arm (0.21.5) + - @esbuild/android-arm (0.25.8) + - @esbuild/android-arm64 (0.21.5) + - @esbuild/android-arm64 (0.25.8) + - @esbuild/android-x64 (0.21.5) + - @esbuild/android-x64 (0.25.8) + - @esbuild/darwin-arm64 (0.21.5) + - @esbuild/darwin-arm64 (0.25.8) + - @esbuild/darwin-x64 (0.21.5) + - @esbuild/darwin-x64 (0.25.8) + - @esbuild/freebsd-arm64 (0.21.5) + - @esbuild/freebsd-arm64 (0.25.8) + - @esbuild/freebsd-x64 (0.21.5) + - @esbuild/freebsd-x64 (0.25.8) + - @esbuild/linux-arm (0.21.5) + - @esbuild/linux-arm (0.25.8) + - @esbuild/linux-arm64 (0.21.5) + - @esbuild/linux-arm64 (0.25.8) + - @esbuild/linux-ia32 (0.21.5) + - @esbuild/linux-ia32 (0.25.8) + - @esbuild/linux-loong64 (0.21.5) + - @esbuild/linux-loong64 (0.25.8) + - @esbuild/linux-mips64el (0.21.5) + - @esbuild/linux-mips64el (0.25.8) + - @esbuild/linux-ppc64 (0.21.5) + - @esbuild/linux-ppc64 (0.25.8) + - @esbuild/linux-riscv64 (0.21.5) + - @esbuild/linux-riscv64 (0.25.8) + - @esbuild/linux-s390x (0.21.5) + - @esbuild/linux-s390x (0.25.8) + - @esbuild/linux-x64 (0.21.5) + - @esbuild/linux-x64 (0.25.8) + - @esbuild/netbsd-arm64 (0.25.8) + - @esbuild/netbsd-x64 (0.21.5) + - @esbuild/netbsd-x64 (0.25.8) + - @esbuild/openbsd-arm64 (0.25.8) + - @esbuild/openbsd-x64 (0.21.5) + - @esbuild/openbsd-x64 (0.25.8) + - @esbuild/openharmony-arm64 (0.25.8) + - @esbuild/sunos-x64 (0.21.5) + - @esbuild/sunos-x64 (0.25.8) + - @esbuild/win32-arm64 (0.21.5) + - @esbuild/win32-arm64 (0.25.8) + - @esbuild/win32-ia32 (0.21.5) + - @esbuild/win32-ia32 (0.25.8) + - @esbuild/win32-x64 (0.21.5) + - @esbuild/win32-x64 (0.25.7) + - @esbuild/win32-x64 (0.25.8) +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - @eslint-community/regexpp (4.12.1) - Toru Nagashima - @floating-ui/core (1.7.2) - atomiks - @floating-ui/dom (1.7.2) - atomiks - @floating-ui/utils (0.2.10) - atomiks - @gar/promisify (1.1.3) - Gar - @homebridge/node-pty-prebuilt-multiarch (0.12.0) - Microsoft Corporation + - @homebridge/node-pty-prebuilt-multiarch (0.13.1) - Microsoft Corporation - @iconify/types (2.0.0) - Vjacheslav Trushkin - @iconify/utils (2.3.0) - Vjacheslav Trushkin - @ioredis/commands (1.2.0) - Zihua Li (http://zihua.li) + - @ioredis/commands (1.3.0) - Zihua Li (http://zihua.li) - @isaacs/balanced-match (4.0.1) - @isaacs/brace-expansion (5.0.0) - @jridgewell/gen-mapping (0.3.12) - Justin Ridgewell @@ -68,8 +133,19 @@ The following packages are licensed under the MIT license: - @malept/flatpak-bundler (0.4.0) - Matt Watson - @mermaid-js/parser (0.6.2) - Yokozuna59 - @modelcontextprotocol/sdk (1.16.0) - Anthropic, PBC (https://anthropic.com) + - @modelcontextprotocol/sdk (1.17.0) - Anthropic, PBC (https://anthropic.com) - @monaco-editor/loader (1.5.0) - Suren Atoyan +<<<<<<< HEAD - @msgpackr-extract/msgpackr-extract-darwin-arm64 (3.0.3) - Kris Zyp +======= + - @monaco-editor/react (4.7.0) - Suren Atoyan + - @msgpackr-extract/msgpackr-extract-darwin-arm64 (3.0.3) - Kris Zyp + - @msgpackr-extract/msgpackr-extract-darwin-x64 (3.0.3) - Kris Zyp + - @msgpackr-extract/msgpackr-extract-linux-arm (3.0.3) - Kris Zyp + - @msgpackr-extract/msgpackr-extract-linux-arm64 (3.0.3) - Kris Zyp + - @msgpackr-extract/msgpackr-extract-linux-x64 (3.0.3) - Kris Zyp + - @msgpackr-extract/msgpackr-extract-win32-x64 (3.0.3) - Kris Zyp +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - @nodelib/fs.scandir (2.1.5) - @nodelib/fs.stat (2.0.5) - @nodelib/fs.walk (1.2.8) @@ -79,13 +155,38 @@ The following packages are licensed under the MIT license: - @radix-ui/primitive (1.1.2) - @radix-ui/rect (1.1.1) - @rolldown/pluginutils (1.0.0-beta.27) +<<<<<<< HEAD - @rollup/rollup-darwin-arm64 (4.45.1) - Lukas Taegert-Atkinson +======= + - @rollup/rollup-android-arm-eabi (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-android-arm64 (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-darwin-arm64 (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-darwin-x64 (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-freebsd-arm64 (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-freebsd-x64 (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-linux-arm-gnueabihf (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-linux-arm-musleabihf (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-linux-arm64-gnu (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-linux-arm64-musl (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-linux-loongarch64-gnu (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-linux-ppc64-gnu (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-linux-riscv64-gnu (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-linux-riscv64-musl (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-linux-s390x-gnu (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-linux-x64-gnu (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-linux-x64-musl (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-win32-arm64-msvc (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-win32-ia32-msvc (4.46.0) - Lukas Taegert-Atkinson + - @rollup/rollup-win32-x64-msvc (4.45.1) - Lukas Taegert-Atkinson + - @rollup/rollup-win32-x64-msvc (4.46.0) - Lukas Taegert-Atkinson +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - @sindresorhus/is (4.6.0) - Sindre Sorhus - @szmarczak/http-timer (4.0.6) - Szymon Marczak - @tootallnate/once (2.0.0) - Nathan Rajlich (http://n8.io/) - @xmldom/xmldom (0.8.10) - @xterm/xterm (5.5.0) - 7zip-bin (5.2.0) + - abort-controller (3.0.0) - Toru Nagashima (https://github.com/mysticatea) - accepts (2.0.0) - acorn (8.15.0) - acorn-jsx (5.3.2) @@ -103,23 +204,34 @@ The following packages are licensed under the MIT license: - ansi-styles (6.2.1) - Sindre Sorhus - any-promise (1.3.0) - Kevin Beaty - app-builder-bin (5.0.0-alpha.10) + - app-builder-bin (5.0.0-alpha.12) - app-builder-lib (25.1.8) - Vladimir Krivosheev +<<<<<<< HEAD - archiver (5.3.2) - Chris Talkington - archiver-utils (2.1.0) - Chris Talkington - archiver-utils (3.0.4) - Chris Talkington - arg (5.0.2) - Josh Junon - aria-hidden (1.2.6) - Anton Korzunov - assert-plus (1.0.0) - Mark Cavage +======= + - app-builder-lib (26.0.12) - Vladimir Krivosheev + - arg (5.0.2) - Josh Junon + - aria-hidden (1.2.6) - Anton Korzunov + - assert-plus (1.0.0) - Mark Cavage + - assertion-error (2.0.1) - Jake Luer (http://qualiancy.com) +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - astral-regex (2.0.0) - Kevin Mårtensson - async (3.2.6) - Caolan McMahon - async-exit-hook (2.0.1) - Tapani Moilanen - asynckit (0.4.0) - Alex Indigo - axios (1.10.0) - Matt Zabriskie + - axios (1.11.0) - Matt Zabriskie - babel-plugin-macros (3.1.0) - Kent C. Dodds (https://kentcdodds.com) - bail (2.0.2) - Titus Wormer (https://wooorm.com) - balanced-match (1.0.2) - Julian Gruber - base64-js (1.5.1) - T. Jameson Little - better-sqlite3 (11.10.0) - Joshua Wise + - better-sqlite3 (12.2.0) - Joshua Wise - binary-extensions (2.3.0) - Sindre Sorhus - bindings (1.5.0) - Nathan Rajlich (http://tootallnate.net) - bl (4.1.0) @@ -135,6 +247,7 @@ The following packages are licensed under the MIT license: - buffer-crc32 (0.2.13) - Brian J. Brennan - buffer-from (1.1.2) - builder-util (25.1.7) - Vladimir Krivosheev + - builder-util (26.0.11) - Vladimir Krivosheev - builder-util-runtime (9.2.10) - Vladimir Krivosheev - builder-util-runtime (9.3.1) - Vladimir Krivosheev - bull (4.16.5) - OptimalBits @@ -171,8 +284,8 @@ The following packages are licensed under the MIT license: - commander (5.1.0) - TJ Holowaychuk - commander (7.2.0) - TJ Holowaychuk - commander (8.3.0) - TJ Holowaychuk + - commander (9.5.0) - TJ Holowaychuk - compare-version (0.1.2) - Kevin Mårtensson - - compress-commons (4.1.2) - Chris Talkington - concat-map (0.0.1) - James Halliday - conf (14.0.0) - Sindre Sorhus - confbox (0.1.8) @@ -184,6 +297,7 @@ The following packages are licensed under the MIT license: - convert-source-map (2.0.0) - Thorsten Lorenz - cookie (0.7.2) - Roman Shtylman - cookie-signature (1.2.2) - TJ Holowaychuk + - copyfiles (2.4.1) - core-util-is (1.0.2) - Isaac Z. Schlueter (http://blog.izs.me/) - core-util-is (1.0.3) - Isaac Z. Schlueter (http://blog.izs.me/) - cors (2.8.5) - Troy Goode (https://github.com/troygoode/) @@ -191,8 +305,12 @@ The following packages are licensed under the MIT license: - cose-base (2.2.0) - cosmiconfig (7.1.0) - David Clark - crc (3.8.0) - Alex Gorbatchev +<<<<<<< HEAD - crc32-stream (4.0.3) - Chris Talkington +======= +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - cron-parser (4.9.0) - Harri Siirak + - cross-dirname (0.1.0) - cross-spawn (7.0.6) - André Cruz - cssesc (3.0.0) - Mathias Bynens - csstype (3.1.3) - Fredrik Nicol @@ -222,15 +340,23 @@ The following packages are licensed under the MIT license: - dir-compare (4.2.0) - Liviu Grigorescu - dlv (1.1.3) - Jason Miller (http://jasonformat.com) - dmg-builder (25.1.8) - Vladimir Krivosheev +<<<<<<< HEAD +======= + - dmg-builder (26.0.12) - Vladimir Krivosheev +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - dmg-license (1.0.11) - argvminusone - dot-prop (9.0.0) - Sindre Sorhus - dunder-proto (1.0.1) - Jordan Harband - eastasianwidth (0.2.0) - Masaki Komagata - ee-first (1.1.1) - Jonathan Ong - electron (36.7.1) - Electron Community + - electron (36.7.3) - Electron Community + - electron (37.2.4) - Electron Community - electron-publish (25.1.7) - Vladimir Krivosheev + - electron-publish (26.0.11) - Vladimir Krivosheev - electron-store (10.1.0) - Sindre Sorhus - electron-updater (6.6.2) - Vladimir Krivosheev + - electron-winstaller (5.4.0) - emoji-regex (8.0.0) - Mathias Bynens - emoji-regex (9.2.2) - Mathias Bynens - encodeurl (2.0.0) @@ -246,12 +372,14 @@ The following packages are licensed under the MIT license: - es-set-tostringtag (2.1.0) - Jordan Harband - es6-error (4.1.1) - Ben Youngblood - esbuild (0.25.7) + - esbuild (0.25.8) - escalade (3.2.0) - Luke Edwards - escape-html (1.0.3) - escape-string-regexp (4.0.0) - Sindre Sorhus - escape-string-regexp (5.0.0) - Sindre Sorhus - estree-util-is-identifier-name (3.0.0) - Titus Wormer (https://wooorm.com) - etag (1.8.1) + - event-target-shim (5.0.1) - Toru Nagashima - eventsource (3.0.7) - Espen Hovlandsdal - eventsource-parser (3.0.3) - Espen Hovlandsdal - express (5.1.0) - TJ Holowaychuk @@ -274,12 +402,18 @@ The following packages are licensed under the MIT license: - flat-cache (4.0.1) - Jared Wray - follow-redirects (1.15.9) - Ruben Verborgh (https://ruben.verborgh.org/) - form-data (4.0.4) - Felix Geisendörfer (http://debuggable.com/) + - form-data-encoder (1.7.2) - Nick K. + - formdata-node (4.4.1) - Nick K. - forwarded (0.2.0) - fraction.js (4.3.7) - Robert Eisele - fresh (2.0.0) - TJ Holowaychuk (http://tjholowaychuk.com) - fs-constants (1.0.0) - Mathias Buus (@mafintosh) - fs-extra (10.1.0) - JP Richardson - fs-extra (11.3.0) - JP Richardson +<<<<<<< HEAD +======= + - fs-extra (7.0.1) - JP Richardson +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - fs-extra (8.1.0) - JP Richardson - fs-extra (9.1.0) - JP Richardson - fsevents (2.3.2) @@ -320,6 +454,7 @@ The following packages are licensed under the MIT license: - imurmurhash (0.1.4) - Jens Taylor - indent-string (4.0.0) - Sindre Sorhus - inline-style-parser (0.2.4) + - interpret (1.4.0) - Gulp Team (http://gulpjs.com/) - ioredis (5.6.1) - Zihua Li (http://zihua.li) - ip-address (9.0.5) - Beau Gunderson (https://beaugunderson.com/) - ipaddr.js (1.9.1) - whitequark @@ -340,6 +475,7 @@ The following packages are licensed under the MIT license: - is-plain-obj (4.1.0) - Sindre Sorhus - is-promise (4.0.0) - ForbesLindesay - is-unicode-supported (0.1.0) - Sindre Sorhus + - isarray (0.0.1) - Julian Gruber - isarray (1.0.0) - Julian Gruber - isbinaryfile (4.0.10) - isbinaryfile (5.0.4) @@ -363,7 +499,6 @@ The following packages are licensed under the MIT license: - layout-base (1.0.2) - layout-base (2.0.1) - lazy-val (1.0.5) - Vladimir Krivosheev - - lazystream (1.0.1) - Jonas Pommerening - levn (0.4.1) - George Zahariev - lilconfig (3.1.3) - antonk52 - lines-and-columns (1.2.4) - Brian Donovan @@ -372,17 +507,18 @@ The following packages are licensed under the MIT license: - lodash (4.17.21) - John-David Dalton - lodash-es (4.17.21) - John-David Dalton - lodash.defaults (4.2.0) - John-David Dalton (http://allyoucanleet.com/) - - lodash.difference (4.5.0) - John-David Dalton (http://allyoucanleet.com/) - lodash.escaperegexp (4.1.2) - John-David Dalton (http://allyoucanleet.com/) - - lodash.flatten (4.4.0) - John-David Dalton (http://allyoucanleet.com/) - lodash.isarguments (3.1.0) - John-David Dalton (http://allyoucanleet.com/) - lodash.isequal (4.5.0) - John-David Dalton (http://allyoucanleet.com/) - - lodash.isplainobject (4.0.6) - John-David Dalton (http://allyoucanleet.com/) - lodash.merge (4.6.2) - John-David Dalton - - lodash.union (4.6.0) - John-David Dalton (http://allyoucanleet.com/) - log-symbols (4.1.0) - Sindre Sorhus - longest-streak (3.1.0) - Titus Wormer (https://wooorm.com) - loose-envify (1.4.0) - Andres Suarez +<<<<<<< HEAD +======= + - loupe (3.1.4) - Veselin Todorov + - loupe (3.2.0) - Veselin Todorov +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - lowercase-keys (2.0.0) - Sindre Sorhus - luxon (3.7.1) - Isaac Cambron - lzma-native (8.0.6) - Anna Henningsen @@ -450,7 +586,9 @@ The following packages are licensed under the MIT license: - mimic-response (3.1.0) - Sindre Sorhus - minimist (1.2.8) - James Halliday - minipass-fetch (2.1.2) - GitHub Inc. + - minipass-fetch (4.0.1) - GitHub Inc. - minizlib (2.1.2) - Isaac Z. Schlueter (http://blog.izs.me/) + - minizlib (3.0.2) - Isaac Z. Schlueter (http://blog.izs.me/) - mlly (1.7.4) - monaco-editor (0.52.2) - Microsoft Corporation - ms (2.1.3) @@ -463,11 +601,21 @@ The following packages are licensed under the MIT license: - negotiator (0.6.4) - negotiator (1.0.0) - node-abi (3.75.0) - Lukas Geiger +<<<<<<< HEAD +======= + - node-abi (4.12.0) - Lukas Geiger +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - node-addon-api (1.7.2) - node-addon-api (3.2.1) - node-addon-api (7.1.1) - node-api-version (0.1.4) - Tim Fish - node-api-version (0.2.1) - Tim Fish +<<<<<<< HEAD +======= + - node-domexception (1.0.0) - Jimmy Wärting + - node-fetch (2.7.0) - David Frank + - node-gyp (11.2.0) - Nathan Rajlich (http://tootallnate.net) +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - node-gyp (9.4.1) - Nathan Rajlich (http://tootallnate.net) - node-gyp-build (4.8.4) - Mathias Buus (@mafintosh) - node-gyp-build-optional-packages (5.2.2) - Mathias Buus (@mafintosh) @@ -487,6 +635,7 @@ The following packages are licensed under the MIT license: - p-limit (3.1.0) - Sindre Sorhus - p-locate (5.0.0) - Sindre Sorhus - p-map (4.0.0) - Sindre Sorhus + - p-map (7.0.3) - Sindre Sorhus - package-manager-detector (1.3.0) - Anthony Fu - parent-module (1.0.1) - Sindre Sorhus - parse-entities (4.0.2) - Titus Wormer (https://wooorm.com) @@ -512,6 +661,7 @@ The following packages are licensed under the MIT license: - plist (3.1.0) - Nathan Rajlich - points-on-curve (0.2.0) - Preet Shihn - points-on-path (0.2.1) - Preet Shihn + - postject (1.0.0-alpha.6) - prebuild-install (7.1.3) - Mathias Buus (@mafintosh) - prelude-ls (1.2.1) - George Zahariev - process-nextick-args (2.0.1) @@ -528,14 +678,27 @@ The following packages are licensed under the MIT license: - quick-lru (5.1.1) - Sindre Sorhus - range-parser (1.2.1) - TJ Holowaychuk (http://tjholowaychuk.com) - raw-body (3.0.0) - Jonathan Ong (http://jongleberry.com) + - react (18.3.1) - react (19.1.0) +<<<<<<< HEAD - react-diff-viewer-continued (3.4.0) +======= + - react-dom (18.3.1) +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - react-dom (19.1.0) - react-is (16.13.1) - react-json-view-lite (1.5.0) - AnyRoad - react-refresh (0.17.0) +<<<<<<< HEAD - read-binary-file-arch (1.0.6) - Samuel Maddock - read-cache (1.0.0) - Bogdan Chadkin +======= + - react-remove-scroll-bar (2.3.8) - Anton Korzunov + - react-style-singleton (2.2.3) - Anton Korzunov (thekashey@gmail.com) + - read-binary-file-arch (1.0.6) - Samuel Maddock + - read-cache (1.0.0) - Bogdan Chadkin + - readable-stream (1.0.34) - Isaac Z. Schlueter (http://blog.izs.me/) +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - readable-stream (2.3.8) - readable-stream (3.6.2) - readdirp (3.6.0) - Thorsten Lorenz (thlorenz.com) @@ -556,12 +719,14 @@ The following packages are licensed under the MIT license: - retry (0.12.0) - Tim Koschützki (http://debuggable.com/) - reusify (1.1.0) - Matteo Collina - rollup (4.45.1) - Rich Harris + - rollup (4.46.0) - Rich Harris - roughjs (4.6.6) - Preet Shihn - router (2.2.0) - Douglas Christopher Wilson - run-parallel (1.2.0) - Feross Aboukhadijeh - safe-buffer (5.1.2) - Feross Aboukhadijeh - safe-buffer (5.2.1) - Feross Aboukhadijeh - safer-buffer (2.1.2) - Nikita Skovoroda + - scheduler (0.23.2) - scheduler (0.26.0) - semver-compare (1.0.0) - James Halliday - send (1.2.0) - TJ Holowaychuk @@ -570,6 +735,7 @@ The following packages are licensed under the MIT license: - shebang-command (2.0.0) - Kevin Mårtensson - shebang-regex (3.0.0) - Sindre Sorhus - shell-quote (1.8.3) - James Halliday + - shx (0.3.4) - side-channel (1.1.0) - Jordan Harband - side-channel-list (1.0.0) - Jordan Harband - side-channel-map (1.0.1) - Jordan Harband @@ -581,6 +747,7 @@ The following packages are licensed under the MIT license: - smart-buffer (4.2.0) - Josh Glazebrook - socks (2.8.6) - Josh Glazebrook - socks-proxy-agent (7.0.0) - Nathan Rajlich + - socks-proxy-agent (8.0.5) - Nathan Rajlich - source-map-support (0.5.21) - space-separated-tokens (2.0.2) - Titus Wormer (https://wooorm.com) - standard-as-callback (2.1.0) - luin @@ -588,6 +755,11 @@ The following packages are licensed under the MIT license: - state-local (1.0.7) - Suren Atoyan - statuses (2.0.1) - statuses (2.0.2) +<<<<<<< HEAD +======= + - std-env (3.9.0) + - string_decoder (0.10.31) +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - string_decoder (1.1.1) - string_decoder (1.3.0) - string-width (4.2.3) - Sindre Sorhus @@ -608,9 +780,12 @@ The following packages are licensed under the MIT license: - tailwind-merge (2.6.0) - Dany Castillo - tar-fs (2.1.3) - Mathias Buus - tar-stream (2.2.0) - Mathias Buus + - temp (0.9.4) - Bruce Williams - temp-file (3.4.0) - Vladimir Krivosheev - thenify (3.3.1) - Jonathan Ong (http://jongleberry.com) - thenify-all (1.6.0) - Jonathan Ong (http://jongleberry.com) + - through2 (2.0.5) - Rod Vagg (https://github.com/rvagg) + - tiny-async-pool (1.3.0) - Rafael Xavier de Souza - tiny-typed-emitter (2.1.0) - Zurab Benashvili - tinyexec (1.0.1) - James Garbutt (https://github.com/43081j) - tinyglobby (0.2.14) - Superchupu @@ -618,6 +793,7 @@ The following packages are licensed under the MIT license: - tmp-promise (3.0.3) - Benjamin Gruenbaum and Collaborators. - to-regex-range (5.0.1) - Jon Schlinkert (https://github.com/jonschlinkert) - toidentifier (1.0.1) - Douglas Christopher Wilson + - tr46 (0.0.3) - Sebastian Mayr - tree-kill (1.2.2) - Peteris Krumins - trim-lines (3.0.1) - Titus Wormer (https://wooorm.com) - trough (2.2.0) - Titus Wormer (https://wooorm.com) @@ -627,7 +803,9 @@ The following packages are licensed under the MIT license: - type-is (2.0.1) - ufo (1.6.1) - uint8array-extras (1.4.0) - Sindre Sorhus + - undici-types (5.26.5) - undici-types (6.21.0) + - undici-types (7.8.0) - unified (11.0.5) - Titus Wormer (https://wooorm.com) - unist-util-is (6.0.0) - Titus Wormer (https://wooorm.com) - unist-util-position (5.0.0) - Titus Wormer (https://wooorm.com) @@ -637,6 +815,7 @@ The following packages are licensed under the MIT license: - universalify (0.1.2) - Ryan Zimmerman - universalify (2.0.1) - Ryan Zimmerman - unpipe (1.0.0) - Douglas Christopher Wilson + - untildify (4.0.0) - Sindre Sorhus - update-browserslist-db (1.1.3) - Andrey Sitnik - util-deprecate (1.0.2) - Nathan Rajlich (http://n8.io/) - uuid (11.1.0) @@ -645,6 +824,7 @@ The following packages are licensed under the MIT license: - verror (1.10.1) - vfile (6.0.3) - Titus Wormer (https://wooorm.com) - vfile-message (4.0.2) - Titus Wormer (https://wooorm.com) + - vfile-message (4.0.3) - Titus Wormer (https://wooorm.com) - vscode-jsonrpc (8.2.0) - Microsoft Corporation - vscode-languageserver (9.0.1) - Microsoft Corporation - vscode-languageserver-protocol (3.17.5) - Microsoft Corporation @@ -652,15 +832,24 @@ The following packages are licensed under the MIT license: - vscode-languageserver-types (3.17.5) - Microsoft Corporation - vscode-uri (3.0.8) - Microsoft - wcwidth (1.0.1) - Tim Oxley +<<<<<<< HEAD +======= + - web-streams-polyfill (3.3.3) - Mattias Buelens + - web-streams-polyfill (4.0.0-beta.3) - Mattias Buelens + - web-streams-polyfill (4.1.0) - Mattias Buelens + - whatwg-url (5.0.0) - Sebastian Mayr +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - when-exit (2.1.4) - word-wrap (1.2.5) - Jon Schlinkert (https://github.com/jonschlinkert) - wrap-ansi (7.0.0) - Sindre Sorhus - wrap-ansi (8.1.0) - Sindre Sorhus - xmlbuilder (15.1.1) - Ozgur Ozcitak + - xtend (4.0.2) - Raynos + - xterm (5.3.0) + - yargs (16.2.0) - yargs (17.7.2) - yauzl (2.10.0) - Josh Wolfe - yocto-queue (0.1.0) - Sindre Sorhus - - zip-stream (4.1.1) - Chris Talkington - zod (3.25.76) - Colin McDonnell - zwitch (2.0.4) - Titus Wormer (https://wooorm.com) @@ -681,16 +870,27 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI The following packages are licensed under the ISC license: - @isaacs/cliui (8.0.2) - Ben Coe + - @isaacs/fs-minipass (4.0.1) - Isaac Z. Schlueter + - @npmcli/agent (3.0.0) - GitHub Inc. - @npmcli/fs (2.1.2) - GitHub Inc. + - @npmcli/fs (4.0.0) - GitHub Inc. - @ungap/structured-clone (1.3.0) - Andrea Giammarchi - abbrev (1.1.1) - Isaac Z. Schlueter + - abbrev (3.0.1) - GitHub Inc. - anymatch (3.1.3) - Elan Shanker - aproba (2.1.0) - Rebecca Turner - are-we-there-yet (3.0.1) - GitHub Inc. - at-least-node (1.0.0) - Ryan Zimmerman - cacache (16.1.3) - GitHub Inc. +<<<<<<< HEAD + - chownr (1.1.4) - Isaac Z. Schlueter (http://blog.izs.me/) + - chownr (2.0.0) - Isaac Z. Schlueter (http://blog.izs.me/) +======= + - cacache (19.0.1) - GitHub Inc. - chownr (1.1.4) - Isaac Z. Schlueter (http://blog.izs.me/) - chownr (2.0.0) - Isaac Z. Schlueter (http://blog.izs.me/) + - cliui (7.0.4) - Ben Coe +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - cliui (8.0.1) - Ben Coe - color-support (1.1.3) - Isaac Z. Schlueter (http://blog.izs.me/) - console-control-strings (1.1.0) - Rebecca Turner (http://re-becca.org/) @@ -726,10 +926,12 @@ The following packages are licensed under the ISC license: - d3-zoom (3.0.0) - Mike Bostock - delaunator (5.0.1) - Vladimir Agafonkin - electron-to-chromium (1.5.187) - Kilian Valkhof + - electron-to-chromium (1.5.191) - Kilian Valkhof - fastq (1.19.1) - Matteo Collina - flatted (3.3.3) - Andrea Giammarchi - foreground-child (3.3.1) - Isaac Z. Schlueter (http://blog.izs.me/) - fs-minipass (2.1.0) - Isaac Z. Schlueter (http://blog.izs.me/) + - fs-minipass (3.0.3) - GitHub Inc. - fs.realpath (1.0.0) - Isaac Z. Schlueter (http://blog.izs.me/) - gauge (4.0.4) - GitHub Inc. - get-caller-file (2.0.5) - Stefan Penner @@ -749,6 +951,7 @@ The following packages are licensed under the ISC license: - internmap (1.0.1) - Mike Bostock - internmap (2.0.3) - Mike Bostock - isexe (2.0.0) - Isaac Z. Schlueter (http://blog.izs.me/) + - isexe (3.1.1) - Isaac Z. Schlueter (http://blog.izs.me/) - json-stringify-safe (5.0.1) - Isaac Z. Schlueter (http://blog.izs.me) - lru-cache (10.4.3) - Isaac Z. Schlueter - lru-cache (11.1.0) - Isaac Z. Schlueter @@ -757,6 +960,7 @@ The following packages are licensed under the ISC license: - lru-cache (7.18.3) - Isaac Z. Schlueter - lucide-react (0.468.0) - Eric Fennis - make-fetch-happen (10.2.1) - GitHub Inc. + - make-fetch-happen (14.0.3) - GitHub Inc. - minimatch (10.0.3) - Isaac Z. Schlueter (http://blog.izs.me) - minimatch (3.1.2) - Isaac Z. Schlueter (http://blog.izs.me) - minimatch (5.1.6) - Isaac Z. Schlueter (http://blog.izs.me) @@ -765,26 +969,49 @@ The following packages are licensed under the ISC license: - minipass (5.0.0) - Isaac Z. Schlueter (http://blog.izs.me/) - minipass (7.1.2) - Isaac Z. Schlueter (http://blog.izs.me/) - minipass-collect (1.0.2) - Isaac Z. Schlueter (https://izs.me) + - minipass-collect (2.0.1) - Isaac Z. Schlueter (https://izs.me) - minipass-flush (1.0.5) - Isaac Z. Schlueter (https://izs.me) - minipass-pipeline (1.2.4) - Isaac Z. Schlueter (https://izs.me) - minipass-sized (1.0.3) - Isaac Z. Schlueter (https://izs.me) + - noms (0.0.0) - Calvin Metcalf - nopt (6.0.0) - GitHub Inc. + - nopt (8.1.0) - GitHub Inc. - npmlog (6.0.2) - GitHub Inc. - once (1.4.0) - Isaac Z. Schlueter (http://blog.izs.me/) - picocolors (1.1.1) - Alexey Raspopov + - proc-log (2.0.1) - GitHub Inc. + - proc-log (5.0.0) - GitHub Inc. - promise-inflight (1.0.1) - Rebecca Turner (http://re-becca.org/) - sax (1.4.1) - Isaac Z. Schlueter (http://blog.izs.me/) +<<<<<<< HEAD +======= + - semver (5.7.2) - GitHub Inc. +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - semver (6.3.1) - GitHub Inc. - semver (7.7.2) - GitHub Inc. - set-blocking (2.0.0) - Ben Coe - setprototypeof (1.2.0) - Wes Todd +<<<<<<< HEAD - signal-exit (3.0.7) - Ben Coe - signal-exit (4.1.0) - Ben Coe - ssri (9.0.1) - GitHub Inc. - tar (6.2.1) - GitHub Inc. +======= + - siginfo (2.0.0) - Emil Bay + - signal-exit (3.0.7) - Ben Coe + - signal-exit (4.1.0) - Ben Coe + - ssri (12.0.0) - GitHub Inc. + - ssri (9.0.1) - GitHub Inc. + - tar (6.2.1) - GitHub Inc. + - tar (7.4.3) - Isaac Z. Schlueter + - test-exclude (7.0.1) - Ben Coe +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - unique-filename (2.0.1) - GitHub Inc. + - unique-filename (4.0.0) - GitHub Inc. - unique-slug (3.0.0) - GitHub Inc. + - unique-slug (5.0.0) - GitHub Inc. - which (2.0.2) - Isaac Z. Schlueter (http://blog.izs.me) + - which (5.0.0) - GitHub Inc. - wide-align (1.1.5) - Rebecca Turner (http://re-becca.org/) - wrappy (1.0.2) - Isaac Z. Schlueter (http://blog.izs.me/) - y18n (5.0.8) - Ben Coe @@ -792,6 +1019,7 @@ The following packages are licensed under the ISC license: - yallist (4.0.0) - Isaac Z. Schlueter (http://blog.izs.me/) - yaml (1.10.2) - Eemeli Aro - yaml (2.8.0) - Eemeli Aro + - yargs-parser (20.2.9) - Ben Coe - yargs-parser (21.1.1) - Ben Coe - zod-to-json-schema (3.24.6) - Stefan Terdell @@ -828,11 +1056,17 @@ The following packages are licensed under the Apache-2.0 license: - @humanwhocodes/retry (0.3.1) - Nicholas C. Zaks - @humanwhocodes/retry (0.4.3) - Nicholas C. Zaks - @img/sharp-darwin-arm64 (0.33.5) - Lovell Fuller +<<<<<<< HEAD +======= + - @img/sharp-darwin-x64 (0.33.5) - Lovell Fuller + - @img/sharp-linux-arm (0.33.5) - Lovell Fuller + - @img/sharp-linux-arm64 (0.33.5) - Lovell Fuller + - @img/sharp-linux-x64 (0.33.5) - Lovell Fuller +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - @malept/cross-spawn-promise (2.0.0) - Mark Lee - chevrotain (11.0.3) - Shahar Soel - class-variance-authority (0.7.1) - Joe Bell (https://joebell.co.uk) - cluster-key-slot (1.1.2) - Mike Diarmid - - crc-32 (1.2.2) - sheetjs - denque (2.1.0) - Invertase - detect-libc (2.0.4) - Lovell Fuller - didyoumean (1.2.2) - Dave Porter @@ -841,7 +1075,7 @@ The following packages are licensed under the Apache-2.0 license: - filelist (1.0.4) - Matthew Eernisse (http://fleegix.org) - jake (10.9.2) - Matthew Eernisse (http://fleegix.org) - openai (5.10.1) - OpenAI - - readdir-glob (1.1.3) - Yann Armelin + - openai (5.10.2) - OpenAI - rxjs (7.8.2) - Ben Lesh - sumchecker (3.0.1) - Mark Lee - ts-interface-checker (0.1.13) - Dmitry S, Grist Labs @@ -1591,6 +1825,39 @@ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- +Package: shelljs +Version: 0.8.5 +Homepage: http://github.com/shelljs/shelljs + +Copyright (c) 2012, Artur Adib +All rights reserved. + +You may use this project under the terms of the New BSD license as follows: + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Artur Adib nor the + names of the contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL ARTUR ADIB BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + Package: source-map Version: 0.5.7 Author: Nick Fitzgerald @@ -1735,7 +2002,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following packages are licensed under the BSD-2-Clause license: - @electron/osx-sign (1.3.1) - electron + - @electron/windows-sign (1.2.2) - Felix Rieseberg - dotenv (16.6.1) + - dotenv (17.2.1) - dotenv-expand (11.0.7) - motdotla - espree (10.4.0) - Nicholas C. Zakas - esrecurse (4.3.0) @@ -1745,6 +2014,7 @@ The following packages are licensed under the BSD-2-Clause license: - http-cache-semantics (4.2.0) - Kornel Lesiński (https://kornel.ski/) - json-schema-typed (8.0.1) - Jeremy Rylan - uri-js (4.4.1) - Gary Court + - webidl-conversions (3.0.1) - Domenic Denicola (https://domenic.me/) Copyright (c) Electron contributors Copyright (c) 2015-2016 Zhuo Lu, Jason Hinkle, et al. @@ -1775,6 +2045,77 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## BlueOak-1.0.0 LICENSE ================================================================================ +Package: chownr +Version: 3.0.0 +Author: Isaac Z. Schlueter (http://blog.izs.me/) +Repository: git://github.com/isaacs/chownr.git + +All packages under `src/` are licensed according to the terms in +their respective `LICENSE` or `LICENSE.md` files. + +The remainder of this project is licensed under the Blue Oak +Model License, as follows: + +----- + +# Blue Oak Model License + +Version 1.0.0 + +## Purpose + +This license gives everyone as much permission to work with +this software as possible, while protecting contributors +from liability. + +## Acceptance + +In order to receive this license, you must agree to its +rules. The rules of this license are both obligations +under that agreement and conditions to your license. +You must not do anything with this software that triggers +a rule that you cannot or will not follow. + +## Copyright + +Each contributor licenses you to do everything with this +software that would otherwise infringe that contributor's +copyright in it. + +## Notices + +You must ensure that everyone who gets a copy of +any part of this software from you, with or without +changes, also gets the text of this license or a link to +. + +## Excuse + +If anyone notifies you in writing that you have not +complied with [Notices](#notices), you can keep your +license by taking all practical steps to comply within 30 +days after the notice. If you do not do so, your license +ends immediately. + +## Patent + +Each contributor licenses you to do everything with this +software that would otherwise infringe any patent claims +they can license or become able to license. + +## Reliability + +No contributor can revoke this license. + +## No Liability + +***As far as the law allows, this software comes as is, +without any warranty or condition, and no contributor +will be liable to anyone for any damages related to this +software or this license, under any kind of legal claim.*** + +-------------------------------------------------------------------------------- + Package: jackspeak Version: 3.4.3 Author: Isaac Z. Schlueter @@ -2098,6 +2439,126 @@ software or this license, under any kind of legal claim.*** -------------------------------------------------------------------------------- +Package: yallist +Version: 5.0.0 +Author: Isaac Z. Schlueter (http://blog.izs.me/) +Repository: git+https://github.com/isaacs/yallist.git + +All packages under `src/` are licensed according to the terms in +their respective `LICENSE` or `LICENSE.md` files. + +The remainder of this project is licensed under the Blue Oak +Model License, as follows: + +----- + +# Blue Oak Model License + +Version 1.0.0 + +## Purpose + +This license gives everyone as much permission to work with +this software as possible, while protecting contributors +from liability. + +## Acceptance + +In order to receive this license, you must agree to its +rules. The rules of this license are both obligations +under that agreement and conditions to your license. +You must not do anything with this software that triggers +a rule that you cannot or will not follow. + +## Copyright + +Each contributor licenses you to do everything with this +software that would otherwise infringe that contributor's +copyright in it. + +## Notices + +You must ensure that everyone who gets a copy of +any part of this software from you, with or without +changes, also gets the text of this license or a link to +. + +## Excuse + +If anyone notifies you in writing that you have not +complied with [Notices](#notices), you can keep your +license by taking all practical steps to comply within 30 +days after the notice. If you do not do so, your license +ends immediately. + +## Patent + +Each contributor licenses you to do everything with this +software that would otherwise infringe any patent claims +they can license or become able to license. + +## Reliability + +No contributor can revoke this license. + +## No Liability + +***As far as the law allows, this software comes as is, +without any warranty or condition, and no contributor +will be liable to anyone for any damages related to this +software or this license, under any kind of legal claim.*** + +-------------------------------------------------------------------------------- + +================================================================================ +## LGPL-3.0-or-later LICENSE +================================================================================ + +Package: @img/sharp-libvips-darwin-arm64 +Version: 1.0.4 +Author: Lovell Fuller +Homepage: https://sharp.pixelplumbing.com + +License: LGPL-3.0-or-later + +-------------------------------------------------------------------------------- + +Package: @img/sharp-libvips-darwin-x64 +Version: 1.0.4 +Author: Lovell Fuller +Homepage: https://sharp.pixelplumbing.com + +License: LGPL-3.0-or-later + +-------------------------------------------------------------------------------- + +Package: @img/sharp-libvips-linux-arm +Version: 1.0.5 +Author: Lovell Fuller +Homepage: https://sharp.pixelplumbing.com + +License: LGPL-3.0-or-later + +-------------------------------------------------------------------------------- + +Package: @img/sharp-libvips-linux-arm64 +Version: 1.0.4 +Author: Lovell Fuller +Homepage: https://sharp.pixelplumbing.com + +License: LGPL-3.0-or-later + +-------------------------------------------------------------------------------- + +Package: @img/sharp-libvips-linux-x64 +Version: 1.0.4 +Author: Lovell Fuller +Homepage: https://sharp.pixelplumbing.com + +License: LGPL-3.0-or-later + +-------------------------------------------------------------------------------- + ================================================================================ ## Unknown LICENSE ================================================================================ @@ -2158,6 +2619,36 @@ DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- +Package: rechoir +Version: 0.6.2 +Author: Tyler Kellen +Homepage: https://github.com/tkellen/node-rechoir + +Copyright (c) 2015 Tyler Kellen + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + Package: stubborn-fs Version: 1.2.5 Repository: github:fabiospampinato/stubborn-fs @@ -2187,6 +2678,31 @@ DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- ================================================================================ +<<<<<<< HEAD +======= +## SEE LICENSE IN README.md LICENSE +================================================================================ + +Package: @anthropic-ai/claude-code +Version: 1.0.56 +Author: Anthropic +Homepage: https://github.com/anthropics/claude-code + +© Anthropic PBC. All rights reserved. Use is subject to Anthropic's [Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms). + +-------------------------------------------------------------------------------- + +Package: @anthropic-ai/claude-code +Version: 1.0.61 +Author: Anthropic +Homepage: https://github.com/anthropics/claude-code + +© Anthropic PBC. All rights reserved. Use is subject to Anthropic's [Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms). + +-------------------------------------------------------------------------------- + +================================================================================ +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) ## (BSD-2-Clause OR MIT OR Apache-2.0) LICENSE ================================================================================ @@ -3540,7 +4056,11 @@ For more information, please refer to ================================================================================ Package: Crystal +<<<<<<< HEAD Version: 0.1.16 +======= +Version: 0.1.17 +>>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) Author: [object Object] License: MIT diff --git a/frontend/src/components/FilePathAutocomplete.tsx b/frontend/src/components/FilePathAutocomplete.tsx index e7f138b8..3c02b8c8 100644 --- a/frontend/src/components/FilePathAutocomplete.tsx +++ b/frontend/src/components/FilePathAutocomplete.tsx @@ -15,7 +15,7 @@ interface FilePathAutocompleteProps { projectId?: string; placeholder?: string; className?: string; - textareaRef?: React.RefObject; + textareaRef?: React.RefObject; isTextarea?: boolean; rows?: number; disabled?: boolean; diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx index eb82300d..fa33a60d 100644 --- a/frontend/src/components/Settings.tsx +++ b/frontend/src/components/Settings.tsx @@ -46,7 +46,7 @@ export function Settings({ isOpen, onClose }: SettingsProps) { const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState<'general' | 'notifications' | 'stravu'>('general'); - const { updateSettings } = useNotifications(); + const { updateSettings, reloadSettings } = useNotifications(); const { theme, toggleTheme } = useTheme(); useEffect(() => { @@ -101,6 +101,12 @@ export function Settings({ isOpen, onClose }: SettingsProps) { // Update the useNotifications hook with new settings updateSettings(notificationSettings); + + // Reload settings from backend to ensure all instances are synced + await reloadSettings(); + + // Dispatch event to notify all instances to reload settings + window.dispatchEvent(new Event('notification-settings-updated')); // Refresh config from server await fetchConfig(); diff --git a/frontend/src/components/session/SessionInput.tsx b/frontend/src/components/session/SessionInput.tsx index a69dc530..1eccb8a7 100644 --- a/frontend/src/components/session/SessionInput.tsx +++ b/frontend/src/components/session/SessionInput.tsx @@ -3,13 +3,14 @@ import { Session } from '../../types/session'; import { ViewMode } from '../../hooks/useSessionView'; import { Cpu } from 'lucide-react'; import { API } from '../../utils/api'; +import { getModifierKeyWithEnter } from '../../utils/platform'; interface SessionInputProps { activeSession: Session; viewMode: ViewMode; input: string; setInput: (input: string) => void; - textareaRef: React.RefObject; + textareaRef: React.RefObject; handleTerminalCommand: () => void; handleSendInput: () => void; handleContinueConversation: (model?: string) => void; @@ -71,9 +72,10 @@ export const SessionInput: React.FC = ({ } }; + const modifierKey = getModifierKeyWithEnter(); const placeholder = viewMode === 'terminal' - ? (activeSession.isRunning ? "Script is running..." : (activeSession.status === 'waiting' ? "Enter your response... (⌘↵ to send)" : "Enter terminal command... (⌘↵ to send)")) - : (activeSession.status === 'waiting' ? "Enter your response... (⌘↵ to send)" : "Continue conversation... (⌘↵ to send)"); + ? (activeSession.isRunning ? "Script is running..." : (activeSession.status === 'waiting' ? `Enter your response... (${modifierKey} to send)` : `Enter terminal command... (${modifierKey} to send)`)) + : (activeSession.status === 'waiting' ? `Enter your response... (${modifierKey} to send)` : `Continue conversation... (${modifierKey} to send)`); return (
diff --git a/frontend/src/components/session/SessionInputWithImages.tsx b/frontend/src/components/session/SessionInputWithImages.tsx index 5792f2c0..aaa469cc 100644 --- a/frontend/src/components/session/SessionInputWithImages.tsx +++ b/frontend/src/components/session/SessionInputWithImages.tsx @@ -8,6 +8,8 @@ import { CommitModePill, AutoCommitSwitch } from '../CommitModeToggle'; import { Dropdown, type DropdownItem } from '../ui/Dropdown'; import { Pill } from '../ui/Pill'; import { SwitchSimple as Switch } from '../ui/SwitchSimple'; +import { getPathTail } from '../../utils/pathUtils'; +import { getModifierKeyWithEnter } from '../../utils/platform'; interface AttachedImage { id: string; @@ -22,7 +24,7 @@ interface SessionInputWithImagesProps { viewMode: ViewMode; input: string; setInput: (input: string) => void; - textareaRef: React.RefObject; + textareaRef: React.RefObject; handleTerminalCommand: () => void; handleSendInput: (attachedImages?: AttachedImage[]) => void; handleContinueConversation: (attachedImages?: AttachedImage[], model?: string) => void; @@ -295,7 +297,7 @@ export const SessionInputWithImages: React.FC = mem - {activeSession.worktreePath.split('/').slice(-3, -2)[0] || 'Project'} + {getPathTail(activeSession.worktreePath, 3).split(/[/\\]/)[0] || 'Project'}
@@ -593,6 +595,7 @@ export const SessionInputWithImages: React.FC = mem : 'bg-surface-secondary hover:bg-surface-hover text-interactive hover:text-interactive-hover border-border-primary focus:ring-interactive' } `} + title={`${buttonConfig.text} (${getModifierKeyWithEnter()})`} > {buttonConfig.text} @@ -600,9 +603,8 @@ export const SessionInputWithImages: React.FC = mem {/* Inline keyboard shortcut */} - ⌘⏎ + {getModifierKeyWithEnter()} )} diff --git a/frontend/src/hooks/useNotifications.ts b/frontend/src/hooks/useNotifications.ts index 6fd16956..6592939d 100644 --- a/frontend/src/hooks/useNotifications.ts +++ b/frontend/src/hooks/useNotifications.ts @@ -67,7 +67,10 @@ export function useNotifications() { }; const showNotification = (title: string, body: string, icon?: string) => { - if (!settings.enabled) return; + if (!settings.enabled) { + console.log('[useNotifications] Notifications disabled, skipping:', title); + return; + } requestPermission().then((hasPermission) => { if (hasPermission) { @@ -125,6 +128,9 @@ export function useNotifications() { return; } + // Debug log current settings state + console.log('[useNotifications] Current settings:', settings); + // Compare current sessions with previous sessions to detect changes sessions.forEach((currentSession) => { const prevSession = prevSessions.find(s => s.id === currentSession.id); @@ -174,28 +180,56 @@ export function useNotifications() { prevSessionsRef.current = sessions; }, [sessions, settings]); - // Load settings on first mount + // Load settings on first mount and listen for updates useEffect(() => { - if (!settingsLoaded.current) { - settingsLoaded.current = true; - - API.config.get().then(response => { + const loadSettings = async () => { + try { + const response = await API.config.get(); if (response.success && response.data?.notifications) { setSettings(response.data.notifications); } - }).catch(error => { + } catch (error) { console.error('Failed to load notification settings:', error); - }); - + } + }; + + if (!settingsLoaded.current) { + settingsLoaded.current = true; + loadSettings(); requestPermission(); } + + // Listen for settings update events + const handleSettingsUpdate = () => { + console.log('[useNotifications] Settings updated, reloading...'); + loadSettings(); + }; + + window.addEventListener('notification-settings-updated', handleSettingsUpdate); + + return () => { + window.removeEventListener('notification-settings-updated', handleSettingsUpdate); + }; }, []); + // Reload settings from backend + const reloadSettings = async () => { + try { + const response = await API.config.get(); + if (response.success && response.data?.notifications) { + setSettings(response.data.notifications); + } + } catch (error) { + console.error('Failed to reload notification settings:', error); + } + }; + return { settings, updateSettings: (newSettings: Partial) => { setSettings(prev => ({ ...prev, ...newSettings })); }, + reloadSettings, requestPermission, showNotification, }; diff --git a/frontend/src/utils/pathUtils.ts b/frontend/src/utils/pathUtils.ts new file mode 100644 index 00000000..11059432 --- /dev/null +++ b/frontend/src/utils/pathUtils.ts @@ -0,0 +1,88 @@ +/** + * Path utilities for the frontend + * These functions handle cross-platform path operations in the browser + */ + +/** + * Get the path separator for the current platform + * This is determined from paths received from the backend + */ +export function getPathSeparator(samplePath?: string): string { + // If we have a sample path, detect the separator + if (samplePath) { + if (samplePath.includes('\\') && !samplePath.includes('/')) { + return '\\'; + } + } + + // Default to forward slash (works for Unix and URLs) + return '/'; +} + +/** + * Get the parent directory of a path + * Works with both Windows and Unix paths + */ +export function getParentPath(filePath: string): string { + const separator = getPathSeparator(filePath); + const parts = filePath.split(separator); + + // Remove the last part (filename or directory) + parts.pop(); + + // Join back together + return parts.join(separator) || ''; +} + +/** + * Get the basename (filename) from a path + * Works with both Windows and Unix paths + */ +export function getBasename(filePath: string): string { + const separator = getPathSeparator(filePath); + const parts = filePath.split(separator); + return parts[parts.length - 1] || ''; +} + +/** + * Join path segments + * Automatically detects the separator to use + */ +export function joinPath(...segments: string[]): string { + if (segments.length === 0) return ''; + + // Detect separator from the first segment that contains one + let separator = '/'; + for (const segment of segments) { + if (segment.includes('\\') && !segment.includes('/')) { + separator = '\\'; + break; + } + } + + // Filter out empty segments and join + return segments.filter(s => s).join(separator); +} + +/** + * Normalize a path (remove redundant separators, etc.) + */ +export function normalizePath(filePath: string): string { + const separator = getPathSeparator(filePath); + + // Split by separator and filter out empty parts + const parts = filePath.split(separator).filter(part => part); + + // Rejoin with consistent separator + return parts.join(separator); +} + +/** + * Get the last N parts of a path + * Useful for displaying shortened paths + */ +export function getPathTail(filePath: string, n: number = 1): string { + const separator = getPathSeparator(filePath); + const parts = filePath.split(separator); + return parts.slice(-n).join(separator); +} \ No newline at end of file diff --git a/frontend/src/utils/platform.ts b/frontend/src/utils/platform.ts new file mode 100644 index 00000000..bd25513b --- /dev/null +++ b/frontend/src/utils/platform.ts @@ -0,0 +1,33 @@ +/** + * Platform detection utilities for the frontend + */ + +/** + * Detect if the current platform is macOS + */ +export function isMac(): boolean { + // Check for Mac using navigator.platform or navigator.userAgent + return /Mac|iPhone|iPod|iPad/i.test(navigator.platform) || + /Mac|iPhone|iPod|iPad/i.test(navigator.userAgent); +} + +/** + * Get the modifier key name for keyboard shortcuts + */ +export function getModifierKey(): string { + return isMac() ? '⌘' : 'Ctrl'; +} + +/** + * Get the modifier key with Enter for keyboard shortcuts + */ +export function getModifierKeyWithEnter(): string { + return isMac() ? '⌘↵' : 'Ctrl+Enter'; +} + +/** + * Get the full modifier key name for display + */ +export function getModifierKeyName(): string { + return isMac() ? 'Command' : 'Ctrl'; +} \ No newline at end of file diff --git a/main/package.json b/main/package.json index 5aa442e6..c79d7b37 100644 --- a/main/package.json +++ b/main/package.json @@ -8,7 +8,7 @@ "dev": "tsc -w", "build": "rimraf dist && tsc && npm run copy:assets && npm run bundle:mcp", "bundle:mcp": "node build-mcp-bridge.js", - "copy:assets": "mkdirp dist/main/src/database/migrations && cp src/database/*.sql dist/main/src/database/ && cp src/database/migrations/*.sql dist/main/src/database/migrations/", + "copy:assets": "shx mkdir -p dist/main/src/database/migrations && shx cp src/database/*.sql dist/main/src/database/ && shx cp src/database/migrations/*.sql dist/main/src/database/migrations/", "lint": "eslint src --ext .ts", "typecheck": "tsc --noEmit", "test": "vitest", @@ -41,6 +41,7 @@ "eslint": "^9.17.0", "mkdirp": "^3.0.1", "rimraf": "^6.0.1", + "shx": "^0.3.4", "typescript": "^5.7.2", "vitest": "^2.1.8" } diff --git a/main/src/database/database.ts b/main/src/database/database.ts index f137e33a..b89684ea 100644 --- a/main/src/database/database.ts +++ b/main/src/database/database.ts @@ -1,6 +1,6 @@ import Database from 'better-sqlite3'; import { readFileSync, mkdirSync } from 'fs'; -import { join, dirname } from 'path'; +import { join, dirname, basename } from 'path'; import type { Project, ProjectRunCommand, Folder, Session, SessionOutput, CreateSessionData, UpdateSessionData, ConversationMessage, PromptMarker, ExecutionDiff, CreateExecutionDiffData } from './models'; export class DatabaseService { @@ -181,7 +181,7 @@ export class DatabaseService { const gitRepoPath = configManager.getGitRepoPath(); if (gitRepoPath) { - const projectName = gitRepoPath.split('/').pop() || 'Default Project'; + const projectName = basename(gitRepoPath) || 'Default Project'; const result = this.db.prepare(` INSERT INTO projects (name, path, active) VALUES (?, ?, 1) diff --git a/main/src/ipc/session.ts b/main/src/ipc/session.ts index 1133dd9b..579c65ec 100644 --- a/main/src/ipc/session.ts +++ b/main/src/ipc/session.ts @@ -195,6 +195,34 @@ export function registerSessionHandlers(ipcMain: IpcMain, services: AppServices) return { success: false, error: 'Session is already archived' }; } + // Stop Claude Code process if it's running (important for Windows to release file locks) + if (claudeCodeManager.isSessionRunning(sessionId)) { + console.log(`[Main] Stopping Claude Code process for session ${sessionId} before archiving`); + try { + await claudeCodeManager.stopSession(sessionId); + // Give Windows time to release file locks + if (process.platform === 'win32') { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } catch (error) { + console.error(`[Main] Failed to stop Claude Code process for session ${sessionId}:`, error); + } + } + + // Close terminal session if it exists (important for Windows to release directory locks) + // This is done here in addition to archiveSession to ensure it happens BEFORE worktree removal + try { + console.log(`[Main] Closing terminal session for ${sessionId} before worktree removal`); + await sessionManager.closeTerminalSession(sessionId); + // Give Windows extra time to release directory locks after terminal closure + if (process.platform === 'win32') { + console.log(`[Main] Waiting 2 seconds for Windows to release terminal directory locks...`); + await new Promise(resolve => setTimeout(resolve, 2000)); + } + } catch (error) { + console.error(`[Main] Failed to close terminal session for ${sessionId}:`, error); + } + // Add a message to session output about archiving const timestamp = new Date().toLocaleTimeString(); let archiveMessage = `\r\n\x1b[36m[${timestamp}]\x1b[0m \x1b[1m\x1b[44m\x1b[37m 📦 ARCHIVING SESSION \x1b[0m\r\n`; diff --git a/main/src/services/worktreeManager.ts b/main/src/services/worktreeManager.ts index aea57b53..6452bc6a 100644 --- a/main/src/services/worktreeManager.ts +++ b/main/src/services/worktreeManager.ts @@ -1,7 +1,8 @@ import { exec } from 'child_process'; import { promisify } from 'util'; -import { join } from 'path'; -import { mkdir } from 'fs/promises'; +import { join, isAbsolute } from 'path'; +import { mkdir, access } from 'fs/promises'; +import * as fs from 'fs/promises'; import { getShellPath } from '../utils/shellPath'; const execAsync = promisify(exec); @@ -32,7 +33,9 @@ export class WorktreeManager { let baseDir: string; // Check if worktreeFolder is an absolute path - if (worktreeFolder && (worktreeFolder.startsWith('/') || worktreeFolder.includes(':'))) { + // On Windows: C:\ or \\server\share + // On Unix: /path + if (worktreeFolder && isAbsolute(worktreeFolder)) { baseDir = worktreeFolder; } else { baseDir = join(projectPath, folderName); @@ -169,18 +172,94 @@ export class WorktreeManager { const worktreePath = join(baseDir, name); try { - await execWithShellPath(`git worktree remove "${worktreePath}" --force`, { cwd: projectPath }); + console.log(`[WorktreeManager] Attempting to remove worktree: ${worktreePath}`); + + // On Windows, check if the directory is locked before attempting removal + if (process.platform === 'win32') { + try { + // Try to access the directory to see if it's locked + await fs.access(worktreePath); + console.log(`[WorktreeManager] Worktree directory exists and is accessible`); + + // Check if any process has this as its working directory + const currentDir = process.cwd(); + if (currentDir.toLowerCase().startsWith(worktreePath.toLowerCase())) { + console.log(`[WorktreeManager] Current process is in worktree directory, changing to project root`); + process.chdir(projectPath); + } + } catch (accessError) { + console.log(`[WorktreeManager] Worktree directory not accessible: ${accessError}`); + } + } + + // First, check if git knows about this worktree + console.log(`[WorktreeManager] Checking if git knows about worktree: ${worktreePath}`); + const listResult = await execWithShellPath(`git worktree list`, { cwd: projectPath }); + console.log(`[WorktreeManager] Current worktrees:`, listResult.stdout); + + const removeCommand = `git worktree remove "${worktreePath}" --force`; + console.log(`[WorktreeManager] Executing: ${removeCommand}`); + const result = await execWithShellPath(removeCommand, { cwd: projectPath }); + console.log(`[WorktreeManager] Command output:`, result.stdout || '(no output)'); + console.log(`[WorktreeManager] Command stderr:`, result.stderr || '(no stderr)'); + console.log(`[WorktreeManager] Successfully removed worktree: ${worktreePath}`); } catch (error: any) { const errorMessage = error.stderr || error.stdout || error.message || String(error); + console.error(`[WorktreeManager] Git worktree remove failed with error:`, errorMessage); + console.error(`[WorktreeManager] Full error object:`, error); // If the worktree is not found, that's okay - it might have been manually deleted if (errorMessage.includes('is not a working tree') || errorMessage.includes('does not exist') || errorMessage.includes('No such file or directory')) { - console.log(`Worktree ${worktreePath} already removed or doesn't exist, skipping...`); + console.log(`[WorktreeManager] Worktree ${worktreePath} already removed or doesn't exist, skipping...`); return; } + // On Windows, provide more specific error handling + if (process.platform === 'win32' && + (errorMessage.includes('cannot remove') || + errorMessage.includes('Permission denied') || + errorMessage.includes('being used by another process'))) { + console.error(`[WorktreeManager] Windows file lock detected. The worktree directory may be in use by another process.`); + console.error(`[WorktreeManager] Worktree path: ${worktreePath}`); + console.error(`[WorktreeManager] You may need to manually delete this directory after closing all programs using it.`); + + // Try to find what's locking the directory (Windows only) + try { + console.log(`[WorktreeManager] Attempting to find processes using the directory...`); + const handleResult = await execWithShellPath(`handle.exe "${worktreePath}" 2>nul`, { cwd: projectPath }); + if (handleResult.stdout) { + console.log(`[WorktreeManager] Processes using directory:`, handleResult.stdout); + } + } catch (handleError) { + // handle.exe might not be available, that's ok + console.log(`[WorktreeManager] Could not run handle.exe (may not be installed)`); + } + + // Try alternative removal method on Windows + try { + console.log(`[WorktreeManager] Attempting alternative removal using rmdir...`); + const rmdirCommand = `rmdir /s /q "${worktreePath}"`; + console.log(`[WorktreeManager] Executing: ${rmdirCommand}`); + const rmdirResult = await execWithShellPath(rmdirCommand, { cwd: projectPath }); + console.log(`[WorktreeManager] rmdir output:`, rmdirResult.stdout || '(no output)'); + console.log(`[WorktreeManager] rmdir stderr:`, rmdirResult.stderr || '(no stderr)'); + console.log(`[WorktreeManager] Successfully removed worktree directory using rmdir`); + + // Clean up git's worktree tracking + try { + await execWithShellPath(`git worktree prune`, { cwd: projectPath }); + } catch (pruneError) { + console.warn(`[WorktreeManager] Failed to prune worktree tracking: ${pruneError}`); + } + + return; + } catch (rmdirError: any) { + console.error(`[WorktreeManager] Alternative removal also failed: ${rmdirError.message}`); + } + } + // For other errors, still throw throw new Error(`Failed to remove worktree: ${errorMessage}`); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2667733e..e86f4b10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -287,6 +287,9 @@ importers: rimraf: specifier: ^6.0.1 version: 6.0.1 + shx: + specifier: ^0.3.4 + version: 0.3.4 typescript: specifier: ^5.7.2 version: 5.8.3 @@ -3179,6 +3182,10 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + ioredis@5.6.1: resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==} engines: {node: '>=12.22.0'} @@ -4218,6 +4225,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -4372,6 +4383,16 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} + shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + + shx@0.3.4: + resolution: {integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==} + engines: {node: '>=6'} + hasBin: true + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -8182,6 +8203,8 @@ snapshots: internmap@2.0.3: {} + interpret@1.4.0: {} + ioredis@5.6.1: dependencies: '@ioredis/commands': 1.2.0 @@ -9425,6 +9448,10 @@ snapshots: dependencies: picomatch: 2.3.1 + rechoir@0.6.2: + dependencies: + resolve: 1.22.10 + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -9631,6 +9658,17 @@ snapshots: shell-quote@1.8.3: {} + shelljs@0.8.5: + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + + shx@0.3.4: + dependencies: + minimist: 1.2.8 + shelljs: 0.8.5 + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 From 4c38f2b8c178d6a48bf7469b266694b088b22daa Mon Sep 17 00:00:00 2001 From: Sallvainian Date: Mon, 28 Jul 2025 20:13:52 -0400 Subject: [PATCH 02/14] Add remaining Windows compatibility changes - Use cross-platform path utilities in AboutDialog, FileEditor, and SessionListItem - Fix TypeScript ref type in useSessionView - Add Windows build configurations to package.json --- frontend/src/components/AboutDialog.tsx | 3 ++- frontend/src/components/FileEditor.tsx | 3 ++- frontend/src/components/SessionListItem.tsx | 3 ++- frontend/src/hooks/useSessionView.ts | 2 +- package.json | 30 +++++++++++++++++++++ 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/AboutDialog.tsx b/frontend/src/components/AboutDialog.tsx index 3b9a84fa..6c9fde42 100644 --- a/frontend/src/components/AboutDialog.tsx +++ b/frontend/src/components/AboutDialog.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { X, ExternalLink, Download, Check, AlertCircle, Loader2 } from 'lucide-react'; import { UpdateDialog } from './UpdateDialog'; +import { getBasename } from '../utils/pathUtils'; interface VersionInfo { current: string; @@ -174,7 +175,7 @@ export function AboutDialog({ isOpen, onClose }: AboutDialogProps) { Working Directory - {versionInfo.workingDirectory.split('/').pop() || versionInfo.workingDirectory} + {getBasename(versionInfo.workingDirectory) || versionInfo.workingDirectory} )} diff --git a/frontend/src/components/FileEditor.tsx b/frontend/src/components/FileEditor.tsx index e744b8b5..908b205e 100644 --- a/frontend/src/components/FileEditor.tsx +++ b/frontend/src/components/FileEditor.tsx @@ -7,6 +7,7 @@ import { useTheme } from '../contexts/ThemeContext'; import { debounce } from '../utils/debounce'; import { MarkdownPreview } from './MarkdownPreview'; import { useResizablePanel } from '../hooks/useResizablePanel'; +import { getParentPath } from '../utils/pathUtils'; interface FileItem { name: string; @@ -218,7 +219,7 @@ function FileTree({ sessionId, onFileSelect, selectedPath }: FileTreeProps) { if (result.success) { // Refresh the parent directory - const parentPath = file.path.split('/').slice(0, -1).join('/') || ''; + const parentPath = getParentPath(file.path); loadFiles(parentPath); // If the deleted file was selected, clear the selection diff --git a/frontend/src/components/SessionListItem.tsx b/frontend/src/components/SessionListItem.tsx index 5828116c..0f2f6b59 100644 --- a/frontend/src/components/SessionListItem.tsx +++ b/frontend/src/components/SessionListItem.tsx @@ -8,6 +8,7 @@ import { API } from '../utils/api'; import { Star, Archive } from 'lucide-react'; import type { Session, GitStatus } from '../types/session'; import { useContextMenu } from '../contexts/ContextMenuContext'; +import { getBasename } from '../utils/pathUtils'; import { IconButton } from './ui/IconButton'; import { cn } from '../utils/cn'; @@ -501,7 +502,7 @@ export const SessionListItem = memo(function SessionListItem({ session, isNested onClose={() => setShowArchiveConfirm(false)} onConfirm={handleConfirmArchive} title={`Archive Session`} - message={`Archive session "${session.name}"? This will:\n\n• Move the session to the archived sessions list\n• Preserve all session history and outputs\n${session.isMainRepo ? '• Close the active Claude Code connection' : `• Remove the git worktree locally (${session.worktreePath?.split('/').pop() || 'worktree'})`}`} + message={`Archive session "${session.name}"? This will:\n\n• Move the session to the archived sessions list\n• Preserve all session history and outputs\n${session.isMainRepo ? '• Close the active Claude Code connection' : `• Remove the git worktree locally (${session.worktreePath ? getBasename(session.worktreePath) : 'worktree'})`}`} confirmText="Archive" confirmButtonClass="bg-amber-600 hover:bg-amber-700 text-white" icon={} diff --git a/frontend/src/hooks/useSessionView.ts b/frontend/src/hooks/useSessionView.ts index 9b761267..36d23215 100644 --- a/frontend/src/hooks/useSessionView.ts +++ b/frontend/src/hooks/useSessionView.ts @@ -1626,7 +1626,7 @@ export const useSessionView = ( setShouldSquash, isWaitingForFirstOutput, elapsedTime, - textareaRef, + textareaRef: textareaRef as React.RefObject, handleSendInput, handleContinueConversation, handleTerminalCommand, diff --git a/package.json b/package.json index 66d739d7..6025d9d0 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,13 @@ "build:linux": "pnpm run build:frontend && pnpm run build:main && pnpm run inject-build-info && pnpm run generate-notices && electron-builder --linux --publish never", "build:linux:ci": "pnpm run build:frontend && pnpm run build:main && pnpm run inject-build-info && pnpm run generate-notices && electron-builder --linux --publish never", "build:mac:ci": "pnpm run build:frontend && pnpm run build:main && pnpm run inject-build-info && pnpm run generate-notices && node scripts/configure-build.js && electron-builder --mac --publish never", + "build:win": "pnpm run build:frontend && pnpm run build:main && pnpm run inject-build-info && pnpm run generate-notices && electron-builder --win --publish never", + "build:win:ci": "pnpm run build:frontend && pnpm run build:main && pnpm run inject-build-info && pnpm run generate-notices && electron-builder --win --publish never", "inject-build-info": "node scripts/inject-build-info.js", "release:mac": "pnpm run build:frontend && pnpm run build:main && pnpm run inject-build-info && pnpm run generate-notices && node scripts/configure-build.js && electron-builder --mac --publish always", "release:mac:universal": "pnpm run build:frontend && pnpm run build:main && pnpm run inject-build-info && pnpm run generate-notices && node scripts/configure-build.js && electron-builder --mac --universal --publish always", "release:linux": "pnpm run build:frontend && pnpm run build:main && pnpm run inject-build-info && pnpm run generate-notices && electron-builder --linux --publish always", + "release:win": "pnpm run build:frontend && pnpm run build:main && pnpm run inject-build-info && pnpm run generate-notices && electron-builder --win --publish always", "preview": "pnpm run --filter frontend preview", "lint": "pnpm run -r lint", "typecheck": "pnpm run -r typecheck", @@ -158,6 +161,33 @@ "appImage": { "artifactName": "${productName}-${version}-linux-${arch}.${ext}" }, + "win": { + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + }, + { + "target": "portable", + "arch": [ + "x64" + ] + } + ], + "icon": "main/assets/icon.ico", + "publisherName": "Crystal Team", + "artifactName": "${productName}-${version}-Windows-${arch}.${ext}" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "artifactName": "${productName}-${version}-Windows-Setup.${ext}" + }, + "portable": { + "artifactName": "${productName}-${version}-Windows-Portable.${ext}" + }, "publish": { "provider": "github", "owner": "stravu", From 9ab42414aab4c25896cd738e454bb5e3002f3cf1 Mon Sep 17 00:00:00 2001 From: Sallvainian Date: Tue, 29 Jul 2025 16:26:29 -0400 Subject: [PATCH 03/14] Add Windows binary building to GitHub Actions workflows --- .github/workflows/build.yml | 10 ++++++++-- .github/workflows/release.yml | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f73aa3b8..e154bc09 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,8 +11,8 @@ jobs: build: strategy: matrix: - # Build both platforms for validation, but use different commands for speed - os: [macos-latest, ubuntu-latest] + # Build all platforms for validation, but use different commands for speed + os: [macos-latest, ubuntu-latest, windows-latest] include: - os: macos-latest build_cmd: build:mac:ci @@ -24,6 +24,10 @@ jobs: artifact_path: | dist-electron/*.deb dist-electron/*.AppImage + - os: windows-latest + build_cmd: build:win:ci + artifact_name: crystal-windows + artifact_path: dist-electron/*.exe runs-on: ${{ matrix.os }} @@ -89,6 +93,7 @@ jobs: # Build frontend and main process in parallel for faster builds (skip if cached) - name: Build frontend and main process + shell: bash run: | # Check if builds exist from cache if [ ! -d "frontend/dist" ] || [ ! -d "main/dist" ]; then @@ -102,6 +107,7 @@ jobs: fi - name: Rebuild native modules (if needed) + shell: bash run: | # Only rebuild if native modules cache missed or build files changed if [ ! -f "main/node_modules/.bin/electron-rebuild" ] || [ ! -f "main/build/Release/better_sqlite3.node" ]; then diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e0a9a5a..fb8bf3f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: release: strategy: matrix: - os: [macos-latest, ubuntu-latest] + os: [macos-latest, ubuntu-latest, windows-latest] include: - os: macos-latest release_cmd: release:mac @@ -25,6 +25,12 @@ jobs: dist-electron/*.deb dist-electron/*.AppImage dist-electron/latest-linux.yml + - os: windows-latest + release_cmd: release:win + artifact_name: crystal-windows + artifact_path: | + dist-electron/*.exe + dist-electron/latest.yml runs-on: ${{ matrix.os }} From e98a3db58b189db1dcb9e7fd5d9a9d162c638193 Mon Sep 17 00:00:00 2001 From: Sallvainian Date: Wed, 30 Jul 2025 11:35:40 -0400 Subject: [PATCH 04/14] Fix Windows build failure: Add bash shell for ls command The ls command doesn't exist on Windows runners, causing builds to fail. Added 'shell: bash' to ensure the command runs in Git Bash. --- .github/workflows/build.yml | 1 + .github/workflows/release.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e154bc09..26125434 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,6 +138,7 @@ jobs: CSC_DISABLE: ${{ (matrix.os == 'macos-latest' && !secrets.APPLE_CERTIFICATE) && 'true' || 'false' }} - name: List build artifacts + shell: bash run: ls -la dist-electron/ # Skip Flatpak build for CI speed - saves 4+ minutes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb8bf3f4..b400937f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,6 +80,7 @@ jobs: CSC_IDENTITY_AUTO_DISCOVERY: ${{ matrix.os == 'macos-latest' && 'true' || 'false' }} - name: List release artifacts + shell: bash run: ls -la dist-electron/ # - name: Build Flatpak From 9a071072cc871cf12c847e0a6a1dcb6963f3bede Mon Sep 17 00:00:00 2001 From: Sallvainian Date: Wed, 30 Jul 2025 11:37:10 -0400 Subject: [PATCH 05/14] Add test workflow for debugging Windows builds This workflow runs on push to our branch without needing approval, allowing us to debug Windows build issues iteratively. --- .github/workflows/test-windows-build.yml | 73 ++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/test-windows-build.yml diff --git a/.github/workflows/test-windows-build.yml b/.github/workflows/test-windows-build.yml new file mode 100644 index 00000000..449109bc --- /dev/null +++ b/.github/workflows/test-windows-build.yml @@ -0,0 +1,73 @@ +name: Test Windows Build + +on: + push: + branches: + - windows-compatibility-clean + workflow_dispatch: + +jobs: + test-build: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.15.1' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Build frontend and main process + shell: bash + run: | + echo "Building frontend and main process..." + pnpm run build:frontend & + pnpm run build:main & + wait + + - name: Rebuild native modules + run: pnpm run electron:rebuild + + - name: Generate NOTICES file + run: pnpm run generate-notices + + - name: Build Windows application + run: pnpm run build:win:ci + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: List build artifacts + shell: bash + run: ls -la dist-electron/ + + - name: Check for exe file + shell: pwsh + run: | + $exeFiles = Get-ChildItem -Path "dist-electron" -Filter "*.exe" + if ($exeFiles.Count -eq 0) { + Write-Error "No .exe file found!" + exit 1 + } + Write-Host "Found .exe files:" + $exeFiles | ForEach-Object { Write-Host $_.FullName } + + - name: Upload Windows artifact + uses: actions/upload-artifact@v4 + with: + name: crystal-windows-test + path: dist-electron/*.exe + if-no-files-found: error \ No newline at end of file From af77071edd2c019d66ec00c27b5fe65cb1d6ce1a Mon Sep 17 00:00:00 2001 From: Sallvainian Date: Mon, 4 Aug 2025 15:53:10 -0400 Subject: [PATCH 06/14] Fix Windows worktree deletion: Rebuild node-pty native module in CI - Add @homebridge/node-pty-prebuilt-multiarch to electron:rebuild command - Ensures node-pty is rebuilt for correct Electron version in CI builds - Fixes issue where terminal sessions weren't properly closed on Windows - This was causing directory locks that prevented worktree removal --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6025d9d0..487fbc2c 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "lint": "pnpm run -r lint", "typecheck": "pnpm run -r typecheck", "postinstall": "electron-builder install-app-deps", - "electron:rebuild": "cd main && electron-rebuild -f -w better-sqlite3", + "electron:rebuild": "cd main && electron-rebuild -f -w better-sqlite3,@homebridge/node-pty-prebuilt-multiarch", "setup": "pnpm install && pnpm run build:main && pnpm run electron:rebuild", "generate-notices": "node scripts/generate-notices.js", "test": "playwright test", From 2480aa0c797f3ddebbb816ed1e9ec5bbc7756a23 Mon Sep 17 00:00:00 2001 From: Sallvainian Date: Mon, 4 Aug 2025 16:24:53 -0400 Subject: [PATCH 07/14] Add Windows-specific Playwright tests and fix CI build issues - Add comprehensive Windows worktree management tests - Fix electron:rebuild to include node-pty for Windows CI builds - Add test:windows npm script for easy test execution - Add PowerShell script for testing Windows binaries - Update CLAUDE.md with file deletion syntax reference This fixes the issue where CI-built Windows binaries couldn't delete worktrees due to node-pty not being rebuilt for the correct Electron version. --- CLAUDE.md | 34 +++++ package.json | 3 +- scripts/test-windows-binary.ps1 | 96 ++++++++++++++ tests/windows-worktree.spec.ts | 225 ++++++++++++++++++++++++++++++++ 4 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 scripts/test-windows-binary.ps1 create mode 100644 tests/windows-worktree.spec.ts diff --git a/CLAUDE.md b/CLAUDE.md index 4ae23502..211ef101 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -713,6 +713,40 @@ In development mode, Crystal automatically captures all frontend console logs an Crystal is an independent project created by [Stravu](https://stravu.com/?utm_source=Crystal&utm_medium=OS&utm_campaign=Crystal&utm_id=1). Claude™ is a trademark of Anthropic, PBC. Crystal is not affiliated with, endorsed by, or sponsored by Anthropic. This tool is designed to work with Claude Code, which must be installed separately. +## File Deletion Syntax Reference + +⚠️ **IMPORTANT**: Use the correct syntax for deleting files based on the shell environment: + +### In Git Bash (Windows) +```bash +# Use forward slashes and rm command +rm "path/to/file.txt" +rm path/to/file.txt + +# For directories +rm -rf "path/to/directory" + +# DO NOT USE: +# - del (Windows cmd command) +# - backslashes in paths +# - C:\path\to\file (use C:/path/to/file or just relative paths) +``` + +### Examples +```bash +# ✅ CORRECT in Git Bash: +rm "tests/windows-binary-e2e.spec.ts" +rm playwright.windows-binary.config.ts +rm -rf test-artifacts/ + +# ❌ WRONG in Git Bash: +del "C:\Users\frank\Projects\crystal\tests\file.ts" # del command not found +rm "C:\Users\frank\Projects\crystal\tests\file.ts" # backslashes cause issues +``` + +### Best Practice +Always use relative paths from the current directory when possible to avoid path separator issues. + ## important-instruction-reminders Do what has been asked; nothing more, nothing less. NEVER create files unless they're absolutely necessary for achieving your goal. diff --git a/package.json b/package.json index 487fbc2c..a9749a1b 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "test:ci": "playwright test --config=playwright.ci.config.ts", "test:ci:minimal": "playwright test --config=playwright.ci.minimal.config.ts", "test:ui": "playwright test --ui", - "test:headed": "playwright test --headed" + "test:headed": "playwright test --headed", + "test:windows": "playwright test windows-worktree.spec.ts" }, "dependencies": { "@anthropic-ai/claude-code": "^1.0.33", diff --git a/scripts/test-windows-binary.ps1 b/scripts/test-windows-binary.ps1 new file mode 100644 index 00000000..c26fb5e5 --- /dev/null +++ b/scripts/test-windows-binary.ps1 @@ -0,0 +1,96 @@ +# PowerShell script to test the Windows binary +param( + [string]$ExePath = ".\dist-electron\Crystal*.exe", + [switch]$DownloadFromCI, + [string]$RunId, + [switch]$Headed, + [string]$TestPattern = "windows-binary" +) + +Write-Host "Crystal Windows Binary Testing Script" -ForegroundColor Cyan +Write-Host "====================================" -ForegroundColor Cyan + +# Function to download artifact from GitHub Actions +function Download-CIArtifact { + param([string]$RunId) + + Write-Host "`nDownloading artifact from GitHub Actions run: $RunId" -ForegroundColor Yellow + + # Download using gh CLI + $artifactName = "crystal-windows-test" + gh run download $RunId -n $artifactName -D "./test-artifacts" -R Sallvainian/crystal + + # Find the exe + $downloadedExe = Get-ChildItem -Path "./test-artifacts" -Filter "*.exe" -Recurse | Select-Object -First 1 + if (-not $downloadedExe) { + throw "No exe file found in downloaded artifacts" + } + + return $downloadedExe.FullName +} + +# Check if we need to download from CI +if ($DownloadFromCI) { + if (-not $RunId) { + Write-Host "Getting latest workflow run..." -ForegroundColor Yellow + $latestRun = gh run list --workflow="test-windows-build.yml" --limit 1 --json databaseId --jq ".[0].databaseId" -R Sallvainian/crystal + $RunId = $latestRun + } + + $ExePath = Download-CIArtifact -RunId $RunId +} + +# Find the exe file +$exeFile = Get-ChildItem -Path $ExePath | Select-Object -First 1 +if (-not $exeFile) { + Write-Host "ERROR: Crystal.exe not found at: $ExePath" -ForegroundColor Red + Write-Host "Build the application first with: pnpm build:win" -ForegroundColor Yellow + exit 1 +} + +Write-Host "`nTesting Crystal executable: $($exeFile.FullName)" -ForegroundColor Green +Write-Host "File size: $([math]::Round($exeFile.Length / 1MB, 2)) MB" -ForegroundColor Gray + +# Set environment variable for tests +$env:CRYSTAL_EXE_PATH = $exeFile.FullName + +# Prepare test command +$testCmd = "pnpm playwright test --config=playwright.windows-binary.config.ts" + +if ($TestPattern) { + $testCmd += " -g `"$TestPattern`"" +} + +if ($Headed) { + $testCmd += " --headed" +} + +Write-Host "`nRunning tests with command:" -ForegroundColor Yellow +Write-Host $testCmd -ForegroundColor White + +# Create test results directory +New-Item -ItemType Directory -Force -Path "test-results/screenshots" | Out-Null + +# Run the tests +Write-Host "`nStarting Playwright tests..." -ForegroundColor Cyan +Invoke-Expression $testCmd + +# Check test results +if ($LASTEXITCODE -eq 0) { + Write-Host "`nAll tests passed! ✅" -ForegroundColor Green +} else { + Write-Host "`nSome tests failed! ❌" -ForegroundColor Red + Write-Host "Check test-results/html-report/index.html for details" -ForegroundColor Yellow + + # Open HTML report + if (Test-Path "test-results/html-report/index.html") { + Start-Process "test-results/html-report/index.html" + } +} + +# Show screenshots if any +$screenshots = Get-ChildItem -Path "test-results/screenshots" -Filter "*.png" -ErrorAction SilentlyContinue +if ($screenshots) { + Write-Host "`nScreenshots captured:" -ForegroundColor Yellow + $screenshots | ForEach-Object { Write-Host " - $($_.Name)" -ForegroundColor Gray } +} \ No newline at end of file diff --git a/tests/windows-worktree.spec.ts b/tests/windows-worktree.spec.ts new file mode 100644 index 00000000..ca116183 --- /dev/null +++ b/tests/windows-worktree.spec.ts @@ -0,0 +1,225 @@ +import { test, expect } from '@playwright/test'; +import * as path from 'path'; +import * as fs from 'fs/promises'; +import * as os from 'os'; +import { execSync } from 'child_process'; + +// Only run these tests on Windows +test.describe('Windows Worktree Management', () => { + test.skip(process.platform !== 'win32', 'Windows-only tests'); + + test('should display worktree status indicators', async ({ page }) => { + // Navigate to the app + await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); + + // Close welcome dialog if present + const getStartedButton = page.locator('button:has-text("Get Started")'); + if (await getStartedButton.isVisible({ timeout: 1000 }).catch(() => false)) { + await getStartedButton.click(); + } + + // Look for session items + const sessionItems = page.locator('.session-item, [data-testid*="session"]'); + const sessionCount = await sessionItems.count(); + + if (sessionCount > 0) { + // Check first session for worktree info + const firstSession = sessionItems.first(); + await expect(firstSession).toBeVisible(); + + // Look for worktree path or status + const worktreeInfo = firstSession.locator('.worktree-path, .worktree-status, [data-worktree]'); + const hasWorktreeInfo = await worktreeInfo.count() > 0; + + if (hasWorktreeInfo) { + console.log('Worktree information found in session UI'); + } + } + + // Take screenshot for debugging + await page.screenshot({ path: 'test-results/windows-worktree-status.png' }); + }); + + test('should handle session deletion on Windows', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); + + // Close welcome dialog if present + const getStartedButton = page.locator('button:has-text("Get Started")'); + if (await getStartedButton.isVisible({ timeout: 1000 }).catch(() => false)) { + await getStartedButton.click(); + } + + // Check if there are any sessions + const sessionItems = page.locator('.session-item, [data-testid*="session"]'); + const sessionCount = await sessionItems.count(); + + if (sessionCount > 0) { + // Hover over first session to show delete button + const firstSession = sessionItems.first(); + await firstSession.hover(); + + // Look for delete button + const deleteButton = firstSession.locator('button[title*="Delete"], button[aria-label*="Delete"], .delete-button'); + + if (await deleteButton.isVisible({ timeout: 2000 }).catch(() => false)) { + // Click delete + await deleteButton.click(); + + // Look for confirmation dialog + const confirmButton = page.locator('button:has-text("Delete"), button:has-text("Confirm")').last(); + if (await confirmButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await confirmButton.click(); + } + + // Wait for deletion + await page.waitForTimeout(3000); + + // Check for any error messages + const errorDialog = page.locator('.error-dialog, [role="alert"], .notification-error'); + const hasError = await errorDialog.isVisible({ timeout: 1000 }).catch(() => false); + + if (hasError) { + const errorText = await errorDialog.textContent(); + console.log('Error during deletion:', errorText); + + // Check if it's a worktree-specific error + if (errorText?.includes('worktree') || errorText?.includes('directory')) { + console.log('Worktree deletion error detected - this is what we\'re testing for'); + } + } else { + console.log('Session deleted successfully'); + } + } + } else { + console.log('No sessions found to test deletion'); + } + + await page.screenshot({ path: 'test-results/windows-session-deletion.png' }); + }); + + test('should show terminal functionality on Windows', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); + + // Close welcome dialog + const getStartedButton = page.locator('button:has-text("Get Started")'); + if (await getStartedButton.isVisible({ timeout: 1000 }).catch(() => false)) { + await getStartedButton.click(); + } + + // Check for terminal tab + const terminalTab = page.locator('button:has-text("Terminal"), [data-testid="terminal-tab"], .tab-terminal'); + + if (await terminalTab.isVisible({ timeout: 5000 }).catch(() => false)) { + await terminalTab.click(); + await page.waitForTimeout(2000); + + // Look for terminal elements + const terminal = page.locator('.xterm, .terminal-container, [data-testid="terminal"]'); + await expect(terminal).toBeVisible({ timeout: 10000 }); + + // Check for terminal input + const terminalInput = page.locator('textarea[placeholder*="command"], .terminal-input, [data-testid="terminal-input"]'); + + if (await terminalInput.isVisible({ timeout: 2000 }).catch(() => false)) { + // Test Windows-specific command + await terminalInput.fill('echo %OS%'); + await terminalInput.press('Enter'); + + await page.waitForTimeout(2000); + + // Check for Windows output + const terminalOutput = page.locator('.terminal-output, .xterm-screen, [data-testid="terminal-output"]'); + const outputText = await terminalOutput.textContent(); + + if (outputText?.includes('Windows_NT')) { + console.log('Windows terminal command executed successfully'); + } + } + } + + await page.screenshot({ path: 'test-results/windows-terminal.png' }); + }); + + test('should handle git operations on Windows', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); + + // Close welcome dialog + const getStartedButton = page.locator('button:has-text("Get Started")'); + if (await getStartedButton.isVisible({ timeout: 1000 }).catch(() => false)) { + await getStartedButton.click(); + } + + // Look for git-related UI elements + const gitButtons = page.locator('button:has-text("Rebase"), button:has-text("Squash"), button:has-text("Git"), .git-actions'); + + if (await gitButtons.first().isVisible({ timeout: 5000 }).catch(() => false)) { + console.log('Git operations UI found'); + + // Try to hover for tooltips + const firstGitButton = gitButtons.first(); + await firstGitButton.hover(); + await page.waitForTimeout(1000); + + // Check for tooltip with git command + const tooltip = page.locator('.tooltip, [role="tooltip"], .git-command-preview'); + if (await tooltip.isVisible({ timeout: 1000 }).catch(() => false)) { + const tooltipText = await tooltip.textContent(); + console.log('Git command preview:', tooltipText); + } + } + + await page.screenshot({ path: 'test-results/windows-git-operations.png' }); + }); + + test('should create new session with worktree', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); + + // Close welcome dialog + const getStartedButton = page.locator('button:has-text("Get Started")'); + if (await getStartedButton.isVisible({ timeout: 1000 }).catch(() => false)) { + await getStartedButton.click(); + } + + // Look for create session button + const createButton = page.locator('button:has-text("Create Session"), button:has-text("New Session"), [data-testid="create-session"]'); + + if (await createButton.isVisible({ timeout: 5000 }).catch(() => false)) { + await createButton.click(); + + // Wait for dialog + await page.waitForTimeout(1000); + + // Fill in prompt + const promptInput = page.locator('textarea[placeholder*="prompt"], textarea[placeholder*="describe"], [data-testid="session-prompt"]'); + if (await promptInput.isVisible({ timeout: 2000 }).catch(() => false)) { + await promptInput.fill('Test Windows worktree creation and file locking'); + + // Look for create button in dialog + const dialogCreateButton = page.locator('dialog button:has-text("Create"), .dialog-create-button'); + if (await dialogCreateButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await dialogCreateButton.click(); + + // Wait for session creation + await page.waitForTimeout(5000); + + // Check for new session + const sessions = page.locator('.session-item, [data-testid*="session"]'); + const newSessionCount = await sessions.count(); + + if (newSessionCount > 0) { + console.log('Session created successfully'); + + // Check for status indicators + const statusIndicator = sessions.first().locator('.status-indicator, .session-status, [data-status]'); + if (await statusIndicator.isVisible({ timeout: 2000 }).catch(() => false)) { + const status = await statusIndicator.getAttribute('data-status') || await statusIndicator.textContent(); + console.log('Session status:', status); + } + } + } + } + } + + await page.screenshot({ path: 'test-results/windows-session-creation.png' }); + }); +}); \ No newline at end of file From 00a1782ba6738f43dd66232ed7e64894960516a8 Mon Sep 17 00:00:00 2001 From: Sallvainian Date: Wed, 6 Aug 2025 07:07:19 -0400 Subject: [PATCH 08/14] Fix Claude output display on Windows - working version - Removed shell wrapper that was causing command prompt pollution - Added filtering for MCP commands that still appear in output - Fixed line break formatting in grouped messages - Removed non-printable characters causing box display --- .claude/settings.local.json | 9 + .grok/settings.json | 3 + NOTICES | 868 +++++------ RichOutputView-upstream.tsx | 1267 +++++++++++++++++ .../src/components/session/RichOutputView.tsx | 5 + main/src/ipc/session.ts | 154 +- main/src/services/claudeCodeManager.ts | 179 ++- 7 files changed, 1948 insertions(+), 537 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 .grok/settings.json create mode 100644 RichOutputView-upstream.tsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..b4e49c67 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:raw.githubusercontent.com)", + "Bash(pnpm run build:main:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.grok/settings.json b/.grok/settings.json new file mode 100644 index 00000000..1dbc7db1 --- /dev/null +++ b/.grok/settings.json @@ -0,0 +1,3 @@ +{ + "model": "grok-4-latest" +} \ No newline at end of file diff --git a/NOTICES b/NOTICES index 0e50eddb..6670dac0 100644 --- a/NOTICES +++ b/NOTICES @@ -15,7 +15,6 @@ The following packages are licensed under the MIT license: - @antfu/install-pkg (1.1.0) - Anthony Fu - @antfu/utils (8.1.1) - Anthony Fu - @anthropic-ai/sdk (0.53.0) - Anthropic - - @anthropic-ai/sdk (0.57.0) - Anthropic - @babel/code-frame (7.27.1) - The Babel Team (https://babel.dev/team) - @babel/compat-data (7.28.0) - The Babel Team (https://babel.dev/team) - @babel/core (7.28.0) - The Babel Team (https://babel.dev/team) @@ -28,26 +27,18 @@ The following packages are licensed under the MIT license: - @babel/helper-validator-identifier (7.27.1) - The Babel Team (https://babel.dev/team) - @babel/helper-validator-option (7.27.1) - The Babel Team (https://babel.dev/team) - @babel/helpers (7.27.6) - The Babel Team (https://babel.dev/team) - - @babel/helpers (7.28.2) - The Babel Team (https://babel.dev/team) - @babel/parser (7.28.0) - The Babel Team (https://babel.dev/team) - @babel/runtime (7.27.6) - The Babel Team (https://babel.dev/team) - - @babel/runtime (7.28.2) - The Babel Team (https://babel.dev/team) - @babel/template (7.27.2) - The Babel Team (https://babel.dev/team) - @babel/traverse (7.28.0) - The Babel Team (https://babel.dev/team) - @babel/types (7.28.1) - The Babel Team (https://babel.dev/team) - - @babel/types (7.28.2) - The Babel Team (https://babel.dev/team) - @bcoe/v8-coverage (0.2.3) - Charles Samborski (https://demurgos.net) - @braintree/sanitize-url (7.1.1) - @develar/schema-utils (2.6.5) - webpack Contrib (https://github.com/webpack-contrib) - - @electron/asar (3.2.18) - @electron/asar (3.4.1) - - @electron/fuses (1.8.0) - Electron Community - @electron/get (2.0.3) - Samuel Attard - - @electron/node-gyp (10.2.0-electron.1) - Nathan Rajlich (http://tootallnate.net) - @electron/notarize (2.5.0) - Samuel Attard - @electron/rebuild (3.6.1) - Paul Betts - - @electron/rebuild (3.7.0) - - @electron/rebuild (4.0.1) - @electron/universal (2.0.1) - Samuel Attard - @emotion/babel-plugin (11.13.5) - Kye Hohenberger - @emotion/cache (11.14.0) @@ -59,73 +50,21 @@ The following packages are licensed under the MIT license: - @emotion/unitless (0.10.0) - @emotion/utils (1.4.2) - @emotion/weak-memoize (0.4.0) -<<<<<<< HEAD - - @esbuild/darwin-arm64 (0.25.7) -======= - - @esbuild/aix-ppc64 (0.21.5) - - @esbuild/aix-ppc64 (0.25.8) - - @esbuild/android-arm (0.21.5) - - @esbuild/android-arm (0.25.8) - - @esbuild/android-arm64 (0.21.5) - - @esbuild/android-arm64 (0.25.8) - - @esbuild/android-x64 (0.21.5) - - @esbuild/android-x64 (0.25.8) - - @esbuild/darwin-arm64 (0.21.5) - - @esbuild/darwin-arm64 (0.25.8) - - @esbuild/darwin-x64 (0.21.5) - - @esbuild/darwin-x64 (0.25.8) - - @esbuild/freebsd-arm64 (0.21.5) - - @esbuild/freebsd-arm64 (0.25.8) - - @esbuild/freebsd-x64 (0.21.5) - - @esbuild/freebsd-x64 (0.25.8) - - @esbuild/linux-arm (0.21.5) - - @esbuild/linux-arm (0.25.8) - - @esbuild/linux-arm64 (0.21.5) - - @esbuild/linux-arm64 (0.25.8) - - @esbuild/linux-ia32 (0.21.5) - - @esbuild/linux-ia32 (0.25.8) - - @esbuild/linux-loong64 (0.21.5) - - @esbuild/linux-loong64 (0.25.8) - - @esbuild/linux-mips64el (0.21.5) - - @esbuild/linux-mips64el (0.25.8) - - @esbuild/linux-ppc64 (0.21.5) - - @esbuild/linux-ppc64 (0.25.8) - - @esbuild/linux-riscv64 (0.21.5) - - @esbuild/linux-riscv64 (0.25.8) - - @esbuild/linux-s390x (0.21.5) - - @esbuild/linux-s390x (0.25.8) - - @esbuild/linux-x64 (0.21.5) - - @esbuild/linux-x64 (0.25.8) - - @esbuild/netbsd-arm64 (0.25.8) - - @esbuild/netbsd-x64 (0.21.5) - - @esbuild/netbsd-x64 (0.25.8) - - @esbuild/openbsd-arm64 (0.25.8) - - @esbuild/openbsd-x64 (0.21.5) - - @esbuild/openbsd-x64 (0.25.8) - - @esbuild/openharmony-arm64 (0.25.8) - - @esbuild/sunos-x64 (0.21.5) - - @esbuild/sunos-x64 (0.25.8) - - @esbuild/win32-arm64 (0.21.5) - - @esbuild/win32-arm64 (0.25.8) - - @esbuild/win32-ia32 (0.21.5) - - @esbuild/win32-ia32 (0.25.8) - @esbuild/win32-x64 (0.21.5) - @esbuild/win32-x64 (0.25.7) - - @esbuild/win32-x64 (0.25.8) ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - @eslint-community/regexpp (4.12.1) - Toru Nagashima - @floating-ui/core (1.7.2) - atomiks - @floating-ui/dom (1.7.2) - atomiks + - @floating-ui/react-dom (2.1.4) - atomiks - @floating-ui/utils (0.2.10) - atomiks - @gar/promisify (1.1.3) - Gar - @homebridge/node-pty-prebuilt-multiarch (0.12.0) - Microsoft Corporation - - @homebridge/node-pty-prebuilt-multiarch (0.13.1) - Microsoft Corporation - @iconify/types (2.0.0) - Vjacheslav Trushkin - @iconify/utils (2.3.0) - Vjacheslav Trushkin - @ioredis/commands (1.2.0) - Zihua Li (http://zihua.li) - - @ioredis/commands (1.3.0) - Zihua Li (http://zihua.li) - @isaacs/balanced-match (4.0.1) - @isaacs/brace-expansion (5.0.0) + - @istanbuljs/schema (0.1.3) - Corey Farrell - @jridgewell/gen-mapping (0.3.12) - Justin Ridgewell - @jridgewell/resolve-uri (3.1.2) - Justin Ridgewell - @jridgewell/sourcemap-codec (1.5.4) - Justin Ridgewell @@ -133,19 +72,9 @@ The following packages are licensed under the MIT license: - @malept/flatpak-bundler (0.4.0) - Matt Watson - @mermaid-js/parser (0.6.2) - Yokozuna59 - @modelcontextprotocol/sdk (1.16.0) - Anthropic, PBC (https://anthropic.com) - - @modelcontextprotocol/sdk (1.17.0) - Anthropic, PBC (https://anthropic.com) - @monaco-editor/loader (1.5.0) - Suren Atoyan -<<<<<<< HEAD - - @msgpackr-extract/msgpackr-extract-darwin-arm64 (3.0.3) - Kris Zyp -======= - @monaco-editor/react (4.7.0) - Suren Atoyan - - @msgpackr-extract/msgpackr-extract-darwin-arm64 (3.0.3) - Kris Zyp - - @msgpackr-extract/msgpackr-extract-darwin-x64 (3.0.3) - Kris Zyp - - @msgpackr-extract/msgpackr-extract-linux-arm (3.0.3) - Kris Zyp - - @msgpackr-extract/msgpackr-extract-linux-arm64 (3.0.3) - Kris Zyp - - @msgpackr-extract/msgpackr-extract-linux-x64 (3.0.3) - Kris Zyp - @msgpackr-extract/msgpackr-extract-win32-x64 (3.0.3) - Kris Zyp ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - @nodelib/fs.scandir (2.1.5) - @nodelib/fs.stat (2.0.5) - @nodelib/fs.walk (1.2.8) @@ -153,40 +82,37 @@ The following packages are licensed under the MIT license: - @pkgjs/parseargs (0.11.0) - @radix-ui/number (1.1.1) - @radix-ui/primitive (1.1.2) + - @radix-ui/react-arrow (1.1.7) + - @radix-ui/react-checkbox (1.3.2) + - @radix-ui/react-collection (1.1.7) + - @radix-ui/react-context (1.1.2) + - @radix-ui/react-dialog (1.1.14) + - @radix-ui/react-direction (1.1.1) + - @radix-ui/react-label (2.1.7) + - @radix-ui/react-popper (1.2.7) + - @radix-ui/react-portal (1.1.9) + - @radix-ui/react-presence (1.1.4) + - @radix-ui/react-primitive (2.1.3) + - @radix-ui/react-select (2.2.5) + - @radix-ui/react-switch (1.2.5) + - @radix-ui/react-tabs (1.1.12) + - @radix-ui/react-use-rect (1.1.1) + - @radix-ui/react-use-size (1.1.1) - @radix-ui/rect (1.1.1) - @rolldown/pluginutils (1.0.0-beta.27) -<<<<<<< HEAD - - @rollup/rollup-darwin-arm64 (4.45.1) - Lukas Taegert-Atkinson -======= - - @rollup/rollup-android-arm-eabi (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-android-arm64 (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-darwin-arm64 (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-darwin-x64 (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-freebsd-arm64 (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-freebsd-x64 (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-linux-arm-gnueabihf (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-linux-arm-musleabihf (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-linux-arm64-gnu (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-linux-arm64-musl (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-linux-loongarch64-gnu (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-linux-ppc64-gnu (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-linux-riscv64-gnu (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-linux-riscv64-musl (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-linux-s390x-gnu (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-linux-x64-gnu (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-linux-x64-musl (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-win32-arm64-msvc (4.46.0) - Lukas Taegert-Atkinson - - @rollup/rollup-win32-ia32-msvc (4.46.0) - Lukas Taegert-Atkinson - @rollup/rollup-win32-x64-msvc (4.45.1) - Lukas Taegert-Atkinson - - @rollup/rollup-win32-x64-msvc (4.46.0) - Lukas Taegert-Atkinson ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - @sindresorhus/is (4.6.0) - Sindre Sorhus - @szmarczak/http-timer (4.0.6) - Szymon Marczak - @tootallnate/once (2.0.0) - Nathan Rajlich (http://n8.io/) + - @vitest/expect (2.1.9) + - @vitest/pretty-format (2.1.9) + - @vitest/runner (2.1.9) + - @vitest/snapshot (2.1.9) + - @vitest/spy (2.1.9) + - @vitest/utils (2.1.9) - @xmldom/xmldom (0.8.10) - @xterm/xterm (5.5.0) - 7zip-bin (5.2.0) - - abort-controller (3.0.0) - Toru Nagashima (https://github.com/mysticatea) - accepts (2.0.0) - acorn (8.15.0) - acorn-jsx (5.3.2) @@ -204,34 +130,19 @@ The following packages are licensed under the MIT license: - ansi-styles (6.2.1) - Sindre Sorhus - any-promise (1.3.0) - Kevin Beaty - app-builder-bin (5.0.0-alpha.10) - - app-builder-bin (5.0.0-alpha.12) - app-builder-lib (25.1.8) - Vladimir Krivosheev -<<<<<<< HEAD - - archiver (5.3.2) - Chris Talkington - - archiver-utils (2.1.0) - Chris Talkington - - archiver-utils (3.0.4) - Chris Talkington - arg (5.0.2) - Josh Junon - aria-hidden (1.2.6) - Anton Korzunov - - assert-plus (1.0.0) - Mark Cavage -======= - - app-builder-lib (26.0.12) - Vladimir Krivosheev - - arg (5.0.2) - Josh Junon - - aria-hidden (1.2.6) - Anton Korzunov - - assert-plus (1.0.0) - Mark Cavage - assertion-error (2.0.1) - Jake Luer (http://qualiancy.com) ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - - astral-regex (2.0.0) - Kevin Mårtensson - async (3.2.6) - Caolan McMahon - async-exit-hook (2.0.1) - Tapani Moilanen - asynckit (0.4.0) - Alex Indigo - axios (1.10.0) - Matt Zabriskie - - axios (1.11.0) - Matt Zabriskie - babel-plugin-macros (3.1.0) - Kent C. Dodds (https://kentcdodds.com) - bail (2.0.2) - Titus Wormer (https://wooorm.com) - balanced-match (1.0.2) - Julian Gruber - base64-js (1.5.1) - T. Jameson Little - better-sqlite3 (11.10.0) - Joshua Wise - - better-sqlite3 (12.2.0) - Joshua Wise - binary-extensions (2.3.0) - Sindre Sorhus - bindings (1.5.0) - Nathan Rajlich (http://tootallnate.net) - bl (4.1.0) @@ -247,11 +158,11 @@ The following packages are licensed under the MIT license: - buffer-crc32 (0.2.13) - Brian J. Brennan - buffer-from (1.1.2) - builder-util (25.1.7) - Vladimir Krivosheev - - builder-util (26.0.11) - Vladimir Krivosheev - builder-util-runtime (9.2.10) - Vladimir Krivosheev - builder-util-runtime (9.3.1) - Vladimir Krivosheev - bull (4.16.5) - OptimalBits - bytes (3.1.2) - TJ Holowaychuk (http://tjholowaychuk.com) + - cac (6.7.14) - egoist <0x142857@gmail.com> - cacheable-lookup (5.0.4) - Szymon Marczak - cacheable-request (7.0.4) - Luke Childs (http://lukechilds.co.uk) - call-bind-apply-helpers (1.0.2) - Jordan Harband @@ -259,11 +170,13 @@ The following packages are licensed under the MIT license: - callsites (3.1.0) - Sindre Sorhus - camelcase-css (2.0.1) - Steven Vachon (https://www.svachon.com/) - ccount (2.0.1) - Titus Wormer (https://wooorm.com) + - chai (5.2.1) - Jake Luer - chalk (4.1.2) - character-entities (2.0.2) - Titus Wormer (https://wooorm.com) - character-entities-html4 (2.1.0) - Titus Wormer (https://wooorm.com) - character-entities-legacy (3.0.0) - Titus Wormer (https://wooorm.com) - character-reference-invalid (2.0.1) - Titus Wormer (https://wooorm.com) + - check-error (2.1.1) - Jake Luer (http://alogicalparadox.com) - chevrotain-allstar (0.3.1) - TypeFox - chokidar (3.6.0) - Paul Miller (https://paulmillr.com) - chromium-pickle-js (0.2.0) @@ -272,7 +185,6 @@ The following packages are licensed under the MIT license: - clean-stack (2.2.0) - Sindre Sorhus - cli-cursor (3.1.0) - Sindre Sorhus - cli-spinners (2.9.2) - Sindre Sorhus - - cli-truncate (2.1.0) - Sindre Sorhus - clone (1.0.4) - Paul Vorbach (http://paul.vorba.ch/) - clone-response (1.0.3) - Luke Childs (http://lukechilds.co.uk) - clsx (2.1.1) - Luke Edwards @@ -284,7 +196,6 @@ The following packages are licensed under the MIT license: - commander (5.1.0) - TJ Holowaychuk - commander (7.2.0) - TJ Holowaychuk - commander (8.3.0) - TJ Holowaychuk - - commander (9.5.0) - TJ Holowaychuk - compare-version (0.1.2) - Kevin Mårtensson - concat-map (0.0.1) - James Halliday - conf (14.0.0) - Sindre Sorhus @@ -297,20 +208,11 @@ The following packages are licensed under the MIT license: - convert-source-map (2.0.0) - Thorsten Lorenz - cookie (0.7.2) - Roman Shtylman - cookie-signature (1.2.2) - TJ Holowaychuk - - copyfiles (2.4.1) - - core-util-is (1.0.2) - Isaac Z. Schlueter (http://blog.izs.me/) - - core-util-is (1.0.3) - Isaac Z. Schlueter (http://blog.izs.me/) - cors (2.8.5) - Troy Goode (https://github.com/troygoode/) - cose-base (1.0.3) - cose-base (2.2.0) - cosmiconfig (7.1.0) - David Clark - - crc (3.8.0) - Alex Gorbatchev -<<<<<<< HEAD - - crc32-stream (4.0.3) - Chris Talkington -======= ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - cron-parser (4.9.0) - Harri Siirak - - cross-dirname (0.1.0) - cross-spawn (7.0.6) - André Cruz - cssesc (3.0.0) - Mathias Bynens - csstype (3.1.3) - Fredrik Nicol @@ -324,6 +226,7 @@ The following packages are licensed under the MIT license: - debug (4.4.1) - Josh Junon (https://github.com/qix-) - decode-named-character-reference (1.2.0) - Titus Wormer (https://wooorm.com) - decompress-response (6.0.0) - Sindre Sorhus + - deep-eql (5.0.2) - Jake Luer - deep-extend (0.6.0) - Viacheslav Lotsmanov - deep-is (0.1.4) - Thorsten Lorenz - defaults (1.0.4) - Elijah Insua @@ -340,23 +243,14 @@ The following packages are licensed under the MIT license: - dir-compare (4.2.0) - Liviu Grigorescu - dlv (1.1.3) - Jason Miller (http://jasonformat.com) - dmg-builder (25.1.8) - Vladimir Krivosheev -<<<<<<< HEAD -======= - - dmg-builder (26.0.12) - Vladimir Krivosheev ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - - dmg-license (1.0.11) - argvminusone - dot-prop (9.0.0) - Sindre Sorhus - dunder-proto (1.0.1) - Jordan Harband - eastasianwidth (0.2.0) - Masaki Komagata - ee-first (1.1.1) - Jonathan Ong - electron (36.7.1) - Electron Community - - electron (36.7.3) - Electron Community - - electron (37.2.4) - Electron Community - electron-publish (25.1.7) - Vladimir Krivosheev - - electron-publish (26.0.11) - Vladimir Krivosheev - electron-store (10.1.0) - Sindre Sorhus - electron-updater (6.6.2) - Vladimir Krivosheev - - electron-winstaller (5.4.0) - emoji-regex (8.0.0) - Mathias Bynens - emoji-regex (9.2.2) - Mathias Bynens - encodeurl (2.0.0) @@ -368,25 +262,25 @@ The following packages are licensed under the MIT license: - error-ex (1.3.2) - es-define-property (1.0.1) - Jordan Harband - es-errors (1.3.0) - Jordan Harband + - es-module-lexer (1.7.0) - Guy Bedford - es-object-atoms (1.1.1) - Jordan Harband - es-set-tostringtag (2.1.0) - Jordan Harband - es6-error (4.1.1) - Ben Youngblood + - esbuild (0.21.5) - esbuild (0.25.7) - - esbuild (0.25.8) - escalade (3.2.0) - Luke Edwards - escape-html (1.0.3) - escape-string-regexp (4.0.0) - Sindre Sorhus - escape-string-regexp (5.0.0) - Sindre Sorhus - estree-util-is-identifier-name (3.0.0) - Titus Wormer (https://wooorm.com) + - estree-walker (3.0.3) - Rich Harris - etag (1.8.1) - - event-target-shim (5.0.1) - Toru Nagashima - eventsource (3.0.7) - Espen Hovlandsdal - eventsource-parser (3.0.3) - Espen Hovlandsdal - express (5.1.0) - TJ Holowaychuk - express-rate-limit (7.5.1) - Nathan Friedly - exsolve (1.0.7) - extend (3.0.2) - Stefan Thomas (http://www.justmoon.net) - - extsprintf (1.4.1) - fast-deep-equal (3.1.3) - Evgeny Poberezkin - fast-glob (3.3.3) - Denis Malinochkin - fast-json-stable-stringify (2.1.0) - James Halliday @@ -402,22 +296,14 @@ The following packages are licensed under the MIT license: - flat-cache (4.0.1) - Jared Wray - follow-redirects (1.15.9) - Ruben Verborgh (https://ruben.verborgh.org/) - form-data (4.0.4) - Felix Geisendörfer (http://debuggable.com/) - - form-data-encoder (1.7.2) - Nick K. - - formdata-node (4.4.1) - Nick K. - forwarded (0.2.0) - fraction.js (4.3.7) - Robert Eisele - fresh (2.0.0) - TJ Holowaychuk (http://tjholowaychuk.com) - fs-constants (1.0.0) - Mathias Buus (@mafintosh) - fs-extra (10.1.0) - JP Richardson - fs-extra (11.3.0) - JP Richardson -<<<<<<< HEAD -======= - - fs-extra (7.0.1) - JP Richardson ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - fs-extra (8.1.0) - JP Richardson - fs-extra (9.1.0) - JP Richardson - - fsevents (2.3.2) - - fsevents (2.3.3) - function-bind (1.1.2) - Raynos - gensync (1.0.0-beta.2) - Logan Smyth - get-intrinsic (1.3.0) - Jordan Harband @@ -438,6 +324,7 @@ The following packages are licensed under the MIT license: - hasown (2.0.2) - Jordan Harband - hast-util-to-jsx-runtime (2.3.6) - Titus Wormer (https://wooorm.com) - hast-util-whitespace (3.0.0) - Titus Wormer (https://wooorm.com) + - html-escaper (2.0.2) - Andrea Giammarchi - html-url-attributes (3.0.1) - Titus Wormer (https://wooorm.com) - http-errors (2.0.0) - Jonathan Ong (http://jongleberry.com) - http-proxy-agent (5.0.0) - Nathan Rajlich (http://n8.io/) @@ -446,7 +333,6 @@ The following packages are licensed under the MIT license: - https-proxy-agent (5.0.1) - Nathan Rajlich (http://n8.io/) - https-proxy-agent (7.0.6) - Nathan Rajlich (http://n8.io/) - humanize-ms (1.2.1) - dead-horse - - iconv-corefoundation (1.1.7) - argv-minus-one - iconv-lite (0.6.3) - Alexander Shtuchkin - ignore (5.3.2) - kael - ignore (7.0.5) - kael @@ -475,8 +361,6 @@ The following packages are licensed under the MIT license: - is-plain-obj (4.1.0) - Sindre Sorhus - is-promise (4.0.0) - ForbesLindesay - is-unicode-supported (0.1.0) - Sindre Sorhus - - isarray (0.0.1) - Julian Gruber - - isarray (1.0.0) - Julian Gruber - isbinaryfile (4.0.10) - isbinaryfile (5.0.4) - jiti (1.21.7) @@ -514,14 +398,13 @@ The following packages are licensed under the MIT license: - log-symbols (4.1.0) - Sindre Sorhus - longest-streak (3.1.0) - Titus Wormer (https://wooorm.com) - loose-envify (1.4.0) - Andres Suarez -<<<<<<< HEAD -======= - loupe (3.1.4) - Veselin Todorov - - loupe (3.2.0) - Veselin Todorov ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - lowercase-keys (2.0.0) - Sindre Sorhus - luxon (3.7.1) - Isaac Cambron - lzma-native (8.0.6) - Anna Henningsen + - magic-string (0.30.17) - Rich Harris + - magicast (0.3.5) + - make-dir (4.0.0) - Sindre Sorhus - markdown-table (3.0.4) - Titus Wormer (https://wooorm.com) - marked (16.1.1) - Christopher Jeffrey - matcher (3.0.0) - Sindre Sorhus @@ -586,9 +469,7 @@ The following packages are licensed under the MIT license: - mimic-response (3.1.0) - Sindre Sorhus - minimist (1.2.8) - James Halliday - minipass-fetch (2.1.2) - GitHub Inc. - - minipass-fetch (4.0.1) - GitHub Inc. - minizlib (2.1.2) - Isaac Z. Schlueter (http://blog.izs.me/) - - minizlib (3.0.2) - Isaac Z. Schlueter (http://blog.izs.me/) - mlly (1.7.4) - monaco-editor (0.52.2) - Microsoft Corporation - ms (2.1.3) @@ -601,21 +482,10 @@ The following packages are licensed under the MIT license: - negotiator (0.6.4) - negotiator (1.0.0) - node-abi (3.75.0) - Lukas Geiger -<<<<<<< HEAD -======= - - node-abi (4.12.0) - Lukas Geiger ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - - node-addon-api (1.7.2) - node-addon-api (3.2.1) - node-addon-api (7.1.1) - node-api-version (0.1.4) - Tim Fish - node-api-version (0.2.1) - Tim Fish -<<<<<<< HEAD -======= - - node-domexception (1.0.0) - Jimmy Wärting - - node-fetch (2.7.0) - David Frank - - node-gyp (11.2.0) - Nathan Rajlich (http://tootallnate.net) ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - node-gyp (9.4.1) - Nathan Rajlich (http://tootallnate.net) - node-gyp-build (4.8.4) - Mathias Buus (@mafintosh) - node-gyp-build-optional-packages (5.2.2) - Mathias Buus (@mafintosh) @@ -635,7 +505,6 @@ The following packages are licensed under the MIT license: - p-limit (3.1.0) - Sindre Sorhus - p-locate (5.0.0) - Sindre Sorhus - p-map (4.0.0) - Sindre Sorhus - - p-map (7.0.3) - Sindre Sorhus - package-manager-detector (1.3.0) - Anthony Fu - parent-module (1.0.1) - Sindre Sorhus - parse-entities (4.0.2) - Titus Wormer (https://wooorm.com) @@ -648,7 +517,9 @@ The following packages are licensed under the MIT license: - path-parse (1.0.7) - Javier Blanco - path-to-regexp (8.2.0) - path-type (4.0.0) - Sindre Sorhus + - pathe (1.1.2) - pathe (2.0.3) + - pathval (2.0.1) - Veselin Todorov - pe-library (0.4.1) - jet - pend (1.2.0) - Andrew Kelley - picomatch (2.3.1) - Jon Schlinkert (https://github.com/jonschlinkert) @@ -661,10 +532,8 @@ The following packages are licensed under the MIT license: - plist (3.1.0) - Nathan Rajlich - points-on-curve (0.2.0) - Preet Shihn - points-on-path (0.2.1) - Preet Shihn - - postject (1.0.0-alpha.6) - prebuild-install (7.1.3) - Mathias Buus (@mafintosh) - prelude-ls (1.2.1) - George Zahariev - - process-nextick-args (2.0.1) - progress (2.0.3) - TJ Holowaychuk - promise-retry (2.0.1) - IndigoUnited (http://indigounited.com) - prop-types (15.8.1) @@ -678,28 +547,14 @@ The following packages are licensed under the MIT license: - quick-lru (5.1.1) - Sindre Sorhus - range-parser (1.2.1) - TJ Holowaychuk (http://tjholowaychuk.com) - raw-body (3.0.0) - Jonathan Ong (http://jongleberry.com) - - react (18.3.1) - react (19.1.0) -<<<<<<< HEAD - - react-diff-viewer-continued (3.4.0) -======= - - react-dom (18.3.1) ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - react-dom (19.1.0) - react-is (16.13.1) - react-json-view-lite (1.5.0) - AnyRoad - react-refresh (0.17.0) -<<<<<<< HEAD - - read-binary-file-arch (1.0.6) - Samuel Maddock - - read-cache (1.0.0) - Bogdan Chadkin -======= - react-remove-scroll-bar (2.3.8) - Anton Korzunov - - react-style-singleton (2.2.3) - Anton Korzunov (thekashey@gmail.com) - read-binary-file-arch (1.0.6) - Samuel Maddock - read-cache (1.0.0) - Bogdan Chadkin - - readable-stream (1.0.34) - Isaac Z. Schlueter (http://blog.izs.me/) ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - - readable-stream (2.3.8) - readable-stream (3.6.2) - readdirp (3.6.0) - Thorsten Lorenz (thlorenz.com) - redis-errors (1.2.0) - Ruben Bridgewater @@ -719,14 +574,11 @@ The following packages are licensed under the MIT license: - retry (0.12.0) - Tim Koschützki (http://debuggable.com/) - reusify (1.1.0) - Matteo Collina - rollup (4.45.1) - Rich Harris - - rollup (4.46.0) - Rich Harris - roughjs (4.6.6) - Preet Shihn - router (2.2.0) - Douglas Christopher Wilson - run-parallel (1.2.0) - Feross Aboukhadijeh - - safe-buffer (5.1.2) - Feross Aboukhadijeh - safe-buffer (5.2.1) - Feross Aboukhadijeh - safer-buffer (2.1.2) - Nikita Skovoroda - - scheduler (0.23.2) - scheduler (0.26.0) - semver-compare (1.0.0) - James Halliday - send (1.2.0) - TJ Holowaychuk @@ -743,24 +595,18 @@ The following packages are licensed under the MIT license: - simple-concat (1.0.1) - Feross Aboukhadijeh - simple-get (4.0.1) - Feross Aboukhadijeh - simple-update-notifier (2.0.0) - alexbrazier - - slice-ansi (3.0.0) - smart-buffer (4.2.0) - Josh Glazebrook - socks (2.8.6) - Josh Glazebrook - socks-proxy-agent (7.0.0) - Nathan Rajlich - - socks-proxy-agent (8.0.5) - Nathan Rajlich - source-map-support (0.5.21) - space-separated-tokens (2.0.2) - Titus Wormer (https://wooorm.com) + - stackback (0.0.2) - Roman Shtylman - standard-as-callback (2.1.0) - luin - stat-mode (1.0.0) - Nathan Rajlich (http://n8.io/) - state-local (1.0.7) - Suren Atoyan - statuses (2.0.1) - statuses (2.0.2) -<<<<<<< HEAD -======= - std-env (3.9.0) - - string_decoder (0.10.31) ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - - string_decoder (1.1.1) - string_decoder (1.3.0) - string-width (4.2.3) - Sindre Sorhus - string-width (5.1.2) - Sindre Sorhus @@ -780,20 +626,21 @@ The following packages are licensed under the MIT license: - tailwind-merge (2.6.0) - Dany Castillo - tar-fs (2.1.3) - Mathias Buus - tar-stream (2.2.0) - Mathias Buus - - temp (0.9.4) - Bruce Williams - temp-file (3.4.0) - Vladimir Krivosheev - thenify (3.3.1) - Jonathan Ong (http://jongleberry.com) - thenify-all (1.6.0) - Jonathan Ong (http://jongleberry.com) - - through2 (2.0.5) - Rod Vagg (https://github.com/rvagg) - - tiny-async-pool (1.3.0) - Rafael Xavier de Souza - tiny-typed-emitter (2.1.0) - Zurab Benashvili + - tinybench (2.9.0) + - tinyexec (0.3.2) - James Garbutt (https://github.com/43081j) - tinyexec (1.0.1) - James Garbutt (https://github.com/43081j) - tinyglobby (0.2.14) - Superchupu + - tinypool (1.1.1) + - tinyrainbow (1.2.0) + - tinyspy (3.0.2) - tmp (0.2.3) - KARASZI István (http://raszi.hu/) - tmp-promise (3.0.3) - Benjamin Gruenbaum and Collaborators. - to-regex-range (5.0.1) - Jon Schlinkert (https://github.com/jonschlinkert) - toidentifier (1.0.1) - Douglas Christopher Wilson - - tr46 (0.0.3) - Sebastian Mayr - tree-kill (1.2.2) - Peteris Krumins - trim-lines (3.0.1) - Titus Wormer (https://wooorm.com) - trough (2.2.0) - Titus Wormer (https://wooorm.com) @@ -803,9 +650,7 @@ The following packages are licensed under the MIT license: - type-is (2.0.1) - ufo (1.6.1) - uint8array-extras (1.4.0) - Sindre Sorhus - - undici-types (5.26.5) - undici-types (6.21.0) - - undici-types (7.8.0) - unified (11.0.5) - Titus Wormer (https://wooorm.com) - unist-util-is (6.0.0) - Titus Wormer (https://wooorm.com) - unist-util-position (5.0.0) - Titus Wormer (https://wooorm.com) @@ -815,16 +660,13 @@ The following packages are licensed under the MIT license: - universalify (0.1.2) - Ryan Zimmerman - universalify (2.0.1) - Ryan Zimmerman - unpipe (1.0.0) - Douglas Christopher Wilson - - untildify (4.0.0) - Sindre Sorhus - update-browserslist-db (1.1.3) - Andrey Sitnik - util-deprecate (1.0.2) - Nathan Rajlich (http://n8.io/) - uuid (11.1.0) - uuid (8.3.2) - vary (1.1.2) - Douglas Christopher Wilson - - verror (1.10.1) - vfile (6.0.3) - Titus Wormer (https://wooorm.com) - vfile-message (4.0.2) - Titus Wormer (https://wooorm.com) - - vfile-message (4.0.3) - Titus Wormer (https://wooorm.com) - vscode-jsonrpc (8.2.0) - Microsoft Corporation - vscode-languageserver (9.0.1) - Microsoft Corporation - vscode-languageserver-protocol (3.17.5) - Microsoft Corporation @@ -832,21 +674,13 @@ The following packages are licensed under the MIT license: - vscode-languageserver-types (3.17.5) - Microsoft Corporation - vscode-uri (3.0.8) - Microsoft - wcwidth (1.0.1) - Tim Oxley -<<<<<<< HEAD -======= - web-streams-polyfill (3.3.3) - Mattias Buelens - - web-streams-polyfill (4.0.0-beta.3) - Mattias Buelens - - web-streams-polyfill (4.1.0) - Mattias Buelens - - whatwg-url (5.0.0) - Sebastian Mayr ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - when-exit (2.1.4) + - why-is-node-running (2.3.0) - Mathias Buus (@mafintosh) - word-wrap (1.2.5) - Jon Schlinkert (https://github.com/jonschlinkert) - wrap-ansi (7.0.0) - Sindre Sorhus - wrap-ansi (8.1.0) - Sindre Sorhus - xmlbuilder (15.1.1) - Ozgur Ozcitak - - xtend (4.0.2) - Raynos - - xterm (5.3.0) - - yargs (16.2.0) - yargs (17.7.2) - yauzl (2.10.0) - Josh Wolfe - yocto-queue (0.1.0) - Sindre Sorhus @@ -870,27 +704,16 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI The following packages are licensed under the ISC license: - @isaacs/cliui (8.0.2) - Ben Coe - - @isaacs/fs-minipass (4.0.1) - Isaac Z. Schlueter - - @npmcli/agent (3.0.0) - GitHub Inc. - @npmcli/fs (2.1.2) - GitHub Inc. - - @npmcli/fs (4.0.0) - GitHub Inc. - @ungap/structured-clone (1.3.0) - Andrea Giammarchi - abbrev (1.1.1) - Isaac Z. Schlueter - - abbrev (3.0.1) - GitHub Inc. - anymatch (3.1.3) - Elan Shanker - aproba (2.1.0) - Rebecca Turner - are-we-there-yet (3.0.1) - GitHub Inc. - at-least-node (1.0.0) - Ryan Zimmerman - cacache (16.1.3) - GitHub Inc. -<<<<<<< HEAD - - chownr (1.1.4) - Isaac Z. Schlueter (http://blog.izs.me/) - - chownr (2.0.0) - Isaac Z. Schlueter (http://blog.izs.me/) -======= - - cacache (19.0.1) - GitHub Inc. - chownr (1.1.4) - Isaac Z. Schlueter (http://blog.izs.me/) - chownr (2.0.0) - Isaac Z. Schlueter (http://blog.izs.me/) - - cliui (7.0.4) - Ben Coe ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - cliui (8.0.1) - Ben Coe - color-support (1.1.3) - Isaac Z. Schlueter (http://blog.izs.me/) - console-control-strings (1.1.0) - Rebecca Turner (http://re-becca.org/) @@ -926,12 +749,10 @@ The following packages are licensed under the ISC license: - d3-zoom (3.0.0) - Mike Bostock - delaunator (5.0.1) - Vladimir Agafonkin - electron-to-chromium (1.5.187) - Kilian Valkhof - - electron-to-chromium (1.5.191) - Kilian Valkhof - fastq (1.19.1) - Matteo Collina - flatted (3.3.3) - Andrea Giammarchi - foreground-child (3.3.1) - Isaac Z. Schlueter (http://blog.izs.me/) - fs-minipass (2.1.0) - Isaac Z. Schlueter (http://blog.izs.me/) - - fs-minipass (3.0.3) - GitHub Inc. - fs.realpath (1.0.0) - Isaac Z. Schlueter (http://blog.izs.me/) - gauge (4.0.4) - GitHub Inc. - get-caller-file (2.0.5) - Stefan Penner @@ -951,7 +772,6 @@ The following packages are licensed under the ISC license: - internmap (1.0.1) - Mike Bostock - internmap (2.0.3) - Mike Bostock - isexe (2.0.0) - Isaac Z. Schlueter (http://blog.izs.me/) - - isexe (3.1.1) - Isaac Z. Schlueter (http://blog.izs.me/) - json-stringify-safe (5.0.1) - Isaac Z. Schlueter (http://blog.izs.me) - lru-cache (10.4.3) - Isaac Z. Schlueter - lru-cache (11.1.0) - Isaac Z. Schlueter @@ -960,7 +780,6 @@ The following packages are licensed under the ISC license: - lru-cache (7.18.3) - Isaac Z. Schlueter - lucide-react (0.468.0) - Eric Fennis - make-fetch-happen (10.2.1) - GitHub Inc. - - make-fetch-happen (14.0.3) - GitHub Inc. - minimatch (10.0.3) - Isaac Z. Schlueter (http://blog.izs.me) - minimatch (3.1.2) - Isaac Z. Schlueter (http://blog.izs.me) - minimatch (5.1.6) - Isaac Z. Schlueter (http://blog.izs.me) @@ -969,49 +788,28 @@ The following packages are licensed under the ISC license: - minipass (5.0.0) - Isaac Z. Schlueter (http://blog.izs.me/) - minipass (7.1.2) - Isaac Z. Schlueter (http://blog.izs.me/) - minipass-collect (1.0.2) - Isaac Z. Schlueter (https://izs.me) - - minipass-collect (2.0.1) - Isaac Z. Schlueter (https://izs.me) - minipass-flush (1.0.5) - Isaac Z. Schlueter (https://izs.me) - minipass-pipeline (1.2.4) - Isaac Z. Schlueter (https://izs.me) - minipass-sized (1.0.3) - Isaac Z. Schlueter (https://izs.me) - - noms (0.0.0) - Calvin Metcalf - nopt (6.0.0) - GitHub Inc. - - nopt (8.1.0) - GitHub Inc. - npmlog (6.0.2) - GitHub Inc. - once (1.4.0) - Isaac Z. Schlueter (http://blog.izs.me/) - picocolors (1.1.1) - Alexey Raspopov - - proc-log (2.0.1) - GitHub Inc. - - proc-log (5.0.0) - GitHub Inc. - promise-inflight (1.0.1) - Rebecca Turner (http://re-becca.org/) - sax (1.4.1) - Isaac Z. Schlueter (http://blog.izs.me/) -<<<<<<< HEAD -======= - - semver (5.7.2) - GitHub Inc. ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - semver (6.3.1) - GitHub Inc. - semver (7.7.2) - GitHub Inc. - set-blocking (2.0.0) - Ben Coe - setprototypeof (1.2.0) - Wes Todd -<<<<<<< HEAD - - signal-exit (3.0.7) - Ben Coe - - signal-exit (4.1.0) - Ben Coe - - ssri (9.0.1) - GitHub Inc. - - tar (6.2.1) - GitHub Inc. -======= - siginfo (2.0.0) - Emil Bay - signal-exit (3.0.7) - Ben Coe - signal-exit (4.1.0) - Ben Coe - - ssri (12.0.0) - GitHub Inc. - ssri (9.0.1) - GitHub Inc. - tar (6.2.1) - GitHub Inc. - - tar (7.4.3) - Isaac Z. Schlueter - test-exclude (7.0.1) - Ben Coe ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - unique-filename (2.0.1) - GitHub Inc. - - unique-filename (4.0.0) - GitHub Inc. - unique-slug (3.0.0) - GitHub Inc. - - unique-slug (5.0.0) - GitHub Inc. - which (2.0.2) - Isaac Z. Schlueter (http://blog.izs.me) - - which (5.0.0) - GitHub Inc. - wide-align (1.1.5) - Rebecca Turner (http://re-becca.org/) - wrappy (1.0.2) - Isaac Z. Schlueter (http://blog.izs.me/) - y18n (5.0.8) - Ben Coe @@ -1019,7 +817,6 @@ The following packages are licensed under the ISC license: - yallist (4.0.0) - Isaac Z. Schlueter (http://blog.izs.me/) - yaml (1.10.2) - Eemeli Aro - yaml (2.8.0) - Eemeli Aro - - yargs-parser (20.2.9) - Ben Coe - yargs-parser (21.1.1) - Ben Coe - zod-to-json-schema (3.24.6) - Stefan Terdell @@ -1055,14 +852,6 @@ The following packages are licensed under the Apache-2.0 license: - @humanwhocodes/module-importer (1.0.1) - Nicholas C. Zaks - @humanwhocodes/retry (0.3.1) - Nicholas C. Zaks - @humanwhocodes/retry (0.4.3) - Nicholas C. Zaks - - @img/sharp-darwin-arm64 (0.33.5) - Lovell Fuller -<<<<<<< HEAD -======= - - @img/sharp-darwin-x64 (0.33.5) - Lovell Fuller - - @img/sharp-linux-arm (0.33.5) - Lovell Fuller - - @img/sharp-linux-arm64 (0.33.5) - Lovell Fuller - - @img/sharp-linux-x64 (0.33.5) - Lovell Fuller ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) - @malept/cross-spawn-promise (2.0.0) - Mark Lee - chevrotain (11.0.3) - Shahar Soel - class-variance-authority (0.7.1) - Joe Bell (https://joebell.co.uk) @@ -1071,11 +860,11 @@ The following packages are licensed under the Apache-2.0 license: - detect-libc (2.0.4) - Lovell Fuller - didyoumean (1.2.2) - Dave Porter - ejs (3.1.10) - Matthew Eernisse (http://fleegix.org) + - expect-type (1.2.2) - exponential-backoff (3.1.2) - Sami Sayegh - filelist (1.0.4) - Matthew Eernisse (http://fleegix.org) - jake (10.9.2) - Matthew Eernisse (http://fleegix.org) - openai (5.10.1) - OpenAI - - openai (5.10.2) - OpenAI - rxjs (7.8.2) - Ben Lesh - sumchecker (3.0.1) - Mark Lee - ts-interface-checker (0.1.13) - Dmitry S, Grist Labs @@ -1705,6 +1494,134 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -------------------------------------------------------------------------------- +Package: istanbul-lib-coverage +Version: 3.2.2 +Author: Krishnan Anantheswaran +Homepage: https://istanbul.js.org/ + +Copyright 2012-2015 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +Package: istanbul-lib-report +Version: 3.0.1 +Author: Krishnan Anantheswaran +Homepage: https://istanbul.js.org/ + +Copyright 2012-2015 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +Package: istanbul-lib-source-maps +Version: 5.0.6 +Author: Krishnan Anantheswaran +Homepage: https://istanbul.js.org/ + +Copyright 2015 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +Package: istanbul-reports +Version: 3.1.7 +Author: Krishnan Anantheswaran +Homepage: https://istanbul.js.org/ + +Copyright 2012-2015 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + Package: joi Version: 17.13.3 Repository: git://github.com/hapijs/joi @@ -2002,9 +1919,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following packages are licensed under the BSD-2-Clause license: - @electron/osx-sign (1.3.1) - electron - - @electron/windows-sign (1.2.2) - Felix Rieseberg - dotenv (16.6.1) - - dotenv (17.2.1) - dotenv-expand (11.0.7) - motdotla - espree (10.4.0) - Nicholas C. Zakas - esrecurse (4.3.0) @@ -2014,7 +1929,6 @@ The following packages are licensed under the BSD-2-Clause license: - http-cache-semantics (4.2.0) - Kornel Lesiński (https://kornel.ski/) - json-schema-typed (8.0.1) - Jeremy Rylan - uri-js (4.4.1) - Gary Court - - webidl-conversions (3.0.1) - Domenic Denicola (https://domenic.me/) Copyright (c) Electron contributors Copyright (c) 2015-2016 Zhuo Lu, Jason Hinkle, et al. @@ -2045,77 +1959,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## BlueOak-1.0.0 LICENSE ================================================================================ -Package: chownr -Version: 3.0.0 -Author: Isaac Z. Schlueter (http://blog.izs.me/) -Repository: git://github.com/isaacs/chownr.git - -All packages under `src/` are licensed according to the terms in -their respective `LICENSE` or `LICENSE.md` files. - -The remainder of this project is licensed under the Blue Oak -Model License, as follows: - ------ - -# Blue Oak Model License - -Version 1.0.0 - -## Purpose - -This license gives everyone as much permission to work with -this software as possible, while protecting contributors -from liability. - -## Acceptance - -In order to receive this license, you must agree to its -rules. The rules of this license are both obligations -under that agreement and conditions to your license. -You must not do anything with this software that triggers -a rule that you cannot or will not follow. - -## Copyright - -Each contributor licenses you to do everything with this -software that would otherwise infringe that contributor's -copyright in it. - -## Notices - -You must ensure that everyone who gets a copy of -any part of this software from you, with or without -changes, also gets the text of this license or a link to -. - -## Excuse - -If anyone notifies you in writing that you have not -complied with [Notices](#notices), you can keep your -license by taking all practical steps to comply within 30 -days after the notice. If you do not do so, your license -ends immediately. - -## Patent - -Each contributor licenses you to do everything with this -software that would otherwise infringe any patent claims -they can license or become able to license. - -## Reliability - -No contributor can revoke this license. - -## No Liability - -***As far as the law allows, this software comes as is, -without any warranty or condition, and no contributor -will be liable to anyone for any damages related to this -software or this license, under any kind of legal claim.*** - --------------------------------------------------------------------------------- - Package: jackspeak Version: 3.4.3 Author: Isaac Z. Schlueter @@ -2439,126 +2282,6 @@ software or this license, under any kind of legal claim.*** -------------------------------------------------------------------------------- -Package: yallist -Version: 5.0.0 -Author: Isaac Z. Schlueter (http://blog.izs.me/) -Repository: git+https://github.com/isaacs/yallist.git - -All packages under `src/` are licensed according to the terms in -their respective `LICENSE` or `LICENSE.md` files. - -The remainder of this project is licensed under the Blue Oak -Model License, as follows: - ------ - -# Blue Oak Model License - -Version 1.0.0 - -## Purpose - -This license gives everyone as much permission to work with -this software as possible, while protecting contributors -from liability. - -## Acceptance - -In order to receive this license, you must agree to its -rules. The rules of this license are both obligations -under that agreement and conditions to your license. -You must not do anything with this software that triggers -a rule that you cannot or will not follow. - -## Copyright - -Each contributor licenses you to do everything with this -software that would otherwise infringe that contributor's -copyright in it. - -## Notices - -You must ensure that everyone who gets a copy of -any part of this software from you, with or without -changes, also gets the text of this license or a link to -. - -## Excuse - -If anyone notifies you in writing that you have not -complied with [Notices](#notices), you can keep your -license by taking all practical steps to comply within 30 -days after the notice. If you do not do so, your license -ends immediately. - -## Patent - -Each contributor licenses you to do everything with this -software that would otherwise infringe any patent claims -they can license or become able to license. - -## Reliability - -No contributor can revoke this license. - -## No Liability - -***As far as the law allows, this software comes as is, -without any warranty or condition, and no contributor -will be liable to anyone for any damages related to this -software or this license, under any kind of legal claim.*** - --------------------------------------------------------------------------------- - -================================================================================ -## LGPL-3.0-or-later LICENSE -================================================================================ - -Package: @img/sharp-libvips-darwin-arm64 -Version: 1.0.4 -Author: Lovell Fuller -Homepage: https://sharp.pixelplumbing.com - -License: LGPL-3.0-or-later - --------------------------------------------------------------------------------- - -Package: @img/sharp-libvips-darwin-x64 -Version: 1.0.4 -Author: Lovell Fuller -Homepage: https://sharp.pixelplumbing.com - -License: LGPL-3.0-or-later - --------------------------------------------------------------------------------- - -Package: @img/sharp-libvips-linux-arm -Version: 1.0.5 -Author: Lovell Fuller -Homepage: https://sharp.pixelplumbing.com - -License: LGPL-3.0-or-later - --------------------------------------------------------------------------------- - -Package: @img/sharp-libvips-linux-arm64 -Version: 1.0.4 -Author: Lovell Fuller -Homepage: https://sharp.pixelplumbing.com - -License: LGPL-3.0-or-later - --------------------------------------------------------------------------------- - -Package: @img/sharp-libvips-linux-x64 -Version: 1.0.4 -Author: Lovell Fuller -Homepage: https://sharp.pixelplumbing.com - -License: LGPL-3.0-or-later - --------------------------------------------------------------------------------- - ================================================================================ ## Unknown LICENSE ================================================================================ @@ -2678,31 +2401,6 @@ DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- ================================================================================ -<<<<<<< HEAD -======= -## SEE LICENSE IN README.md LICENSE -================================================================================ - -Package: @anthropic-ai/claude-code -Version: 1.0.56 -Author: Anthropic -Homepage: https://github.com/anthropics/claude-code - -© Anthropic PBC. All rights reserved. Use is subject to Anthropic's [Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms). - --------------------------------------------------------------------------------- - -Package: @anthropic-ai/claude-code -Version: 1.0.61 -Author: Anthropic -Homepage: https://github.com/anthropics/claude-code - -© Anthropic PBC. All rights reserved. Use is subject to Anthropic's [Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms). - --------------------------------------------------------------------------------- - -================================================================================ ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) ## (BSD-2-Clause OR MIT OR Apache-2.0) LICENSE ================================================================================ @@ -3317,6 +3015,209 @@ Exhibit B - “Incompatible With Secondary Licenses” Notice -------------------------------------------------------------------------------- +================================================================================ +## Apache-2.0 AND LGPL-3.0-or-later LICENSE +================================================================================ + +Package: @img/sharp-win32-x64 +Version: 0.33.5 +Author: Lovell Fuller +Homepage: https://sharp.pixelplumbing.com + +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------------------------------------- + ================================================================================ ## CC-BY-4.0 LICENSE ================================================================================ @@ -3724,19 +3625,6 @@ Creative Commons may be contacted at creativecommons.org. -------------------------------------------------------------------------------- -================================================================================ -## LGPL-3.0-or-later LICENSE -================================================================================ - -Package: @img/sharp-libvips-darwin-arm64 -Version: 1.0.4 -Author: Lovell Fuller -Homepage: https://sharp.pixelplumbing.com - -License: LGPL-3.0-or-later - --------------------------------------------------------------------------------- - ================================================================================ ## Python-2.0 LICENSE ================================================================================ @@ -4056,11 +3944,7 @@ For more information, please refer to ================================================================================ Package: Crystal -<<<<<<< HEAD -Version: 0.1.16 -======= Version: 0.1.17 ->>>>>>> 7a905d2 (Add Windows compatibility and fix notification settings synchronization) Author: [object Object] License: MIT diff --git a/RichOutputView-upstream.tsx b/RichOutputView-upstream.tsx new file mode 100644 index 00000000..67ba0510 --- /dev/null +++ b/RichOutputView-upstream.tsx @@ -0,0 +1,1267 @@ +import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; +import { API } from '../../utils/api'; +import { MarkdownPreview } from '../MarkdownPreview'; +import { User, Bot, ChevronDown, ChevronRight, Eye, EyeOff, Settings2, Wrench, CheckCircle, XCircle, Clock, ArrowDown } from 'lucide-react'; +import { parseTimestamp, formatDistanceToNow } from '../../utils/timestampUtils'; +import { ThinkingPlaceholder, InlineWorkingIndicator } from './ThinkingPlaceholder'; + +// Agent-agnostic message types for flexibility +interface RawMessage { + id?: string; + type: 'user' | 'assistant' | 'system' | 'tool_use' | 'tool_result' | 'result'; + role?: 'user' | 'assistant' | 'system'; + content?: string | any; + message?: { content?: string | any; [key: string]: any }; + timestamp: string; + name?: string; + input?: any; + tool_use_id?: string; + [key: string]: any; +} + +// Structured message representation for rendering +interface ConversationMessage { + id: string; + role: 'user' | 'assistant' | 'system'; + timestamp: string; + segments: MessageSegment[]; + metadata?: { + agent?: string; + model?: string; + duration?: number; + tokens?: number; + cost?: number; + systemSubtype?: string; + sessionInfo?: any; + }; +} + +// Different types of content within a message +type MessageSegment = + | { type: 'text'; content: string } + | { type: 'tool_call'; tool: ToolCall } + | { type: 'system_info'; info: any } + | { type: 'thinking'; content: string }; + +interface ToolCall { + id: string; + name: string; + input?: any; + result?: ToolResult; + status: 'pending' | 'success' | 'error'; +} + + +interface ToolResult { + content: string; + isError?: boolean; +} + +interface RichOutputViewProps { + sessionId: string; + sessionStatus?: string; + settings?: RichOutputSettings; +} + +// Settings stored in localStorage for persistence +export interface RichOutputSettings { + showToolCalls: boolean; + compactMode: boolean; + collapseTools: boolean; + showThinking: boolean; + showSessionInit: boolean; +} + +const defaultSettings: RichOutputSettings = { + showToolCalls: true, + compactMode: false, + collapseTools: false, + showThinking: true, + showSessionInit: false, // Hide by default - it's developer info +}; + +export const RichOutputView = React.forwardRef<{ scrollToPrompt: (promptIndex: number) => void }, RichOutputViewProps>( + ({ sessionId, sessionStatus, settings: propsSettings }, ref) => { + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [collapsedMessages, setCollapsedMessages] = useState>(new Set()); + const [expandedTools, setExpandedTools] = useState>(new Set()); + const [showScrollButton, setShowScrollButton] = useState(false); + + // Use parent-controlled settings if provided, otherwise use default + const localSettings = useMemo(() => { + const saved = localStorage.getItem('richOutputSettings'); + return saved ? JSON.parse(saved) : defaultSettings; + }, []); + + const settings = propsSettings || localSettings; + + const messagesEndRef = useRef(null); + const scrollContainerRef = useRef(null); + const isLoadingRef = useRef(false); + const userMessageRefs = useRef>(new Map()); + const wasAtBottomRef = useRef(false); + + // Save local settings to localStorage when they change + useEffect(() => { + if (!propsSettings) { + localStorage.setItem('richOutputSettings', JSON.stringify(localSettings)); + } + }, [localSettings, propsSettings]); + + // Expose scroll method via ref + React.useImperativeHandle(ref, () => ({ + scrollToPrompt: (promptIndex: number) => { + const messageDiv = userMessageRefs.current.get(promptIndex); + if (messageDiv && scrollContainerRef.current) { + // Scroll to the message with some offset from top + messageDiv.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + // Add a highlight effect + messageDiv.classList.add('highlight-prompt'); + setTimeout(() => { + messageDiv.classList.remove('highlight-prompt'); + }, 2000); + } + } + }), []); + + // Extract text content from various message formats + const extractTextContent = (msg: RawMessage): string => { + // Handle Claude format: message.content as array of content blocks + if (msg.message?.content && Array.isArray(msg.message.content)) { + return msg.message.content + .filter(block => block.type === 'text') + .map(block => block.text || '') + .join('\n') + .trim(); + } + + // Handle direct string content + if (typeof msg.message?.content === 'string') { + return msg.message.content.trim(); + } + + // Handle direct content field + if (typeof msg.content === 'string') { + return msg.content.trim(); + } + + // Handle Gemini/other formats (future-proofing) + if (msg.message?.parts && Array.isArray(msg.message.parts)) { + return msg.message.parts + .filter(part => part.text) + .map(part => part.text) + .join('\n') + .trim(); + } + + return ''; + }; + + // Detect agent/model from message structure + const detectAgent = (msg: RawMessage): string => { + if (msg.message?.model?.includes('claude')) return 'claude'; + if (msg.message?.model?.includes('gemini')) return 'gemini'; + if (msg.message?.model?.includes('gpt')) return 'gpt-4'; + + // Fallback based on message structure + if (msg.message?.content && Array.isArray(msg.message.content)) return 'claude'; + if (msg.message?.parts) return 'gemini'; + + return 'unknown'; + }; + + // Transform raw messages into structured conversation messages + const transformMessages = (rawMessages: RawMessage[]): ConversationMessage[] => { + const transformed: ConversationMessage[] = []; + + // First pass: Build tool result map + const toolResults = new Map(); + rawMessages.forEach(msg => { + if (msg.type === 'user' && msg.message?.content && Array.isArray(msg.message.content)) { + msg.message.content.forEach((block: any) => { + if (block.type === 'tool_result' && block.tool_use_id) { + toolResults.set(block.tool_use_id, { + content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content), + isError: block.is_error || false + }); + } + }); + } + }); + + // Second pass: Build conversation messages + for (let i = 0; i < rawMessages.length; i++) { + const msg = rawMessages[i]; + + if (msg.type === 'user') { + // Check if this is a tool result message + let hasToolResult = false; + let hasOnlyText = true; + + if (msg.message?.content && Array.isArray(msg.message.content)) { + hasToolResult = msg.message.content.some((block: any) => block.type === 'tool_result'); + hasOnlyText = msg.message.content.every((block: any) => block.type === 'text'); + } + + // Only show real user prompts (text-only messages without tool results) + if (!hasToolResult && hasOnlyText) { + const textContent = extractTextContent(msg); + + if (textContent) { + const userMessage = { + id: msg.id || `user-${i}-${msg.timestamp}`, + role: 'user' as const, + timestamp: msg.timestamp, + segments: [{ type: 'text', content: textContent } as MessageSegment], + metadata: { agent: detectAgent(msg) } + }; + + transformed.push(userMessage); + + // Removed: Commit summaries are now shown in the dedicated Commits panel + } + } + // Skip tool result messages - they're attached to assistant messages + + } else if (msg.type === 'assistant') { + const segments: MessageSegment[] = []; + + + // Check for direct text field first (some messages come this way) + if (msg.text && typeof msg.text === 'string') { + segments.push({ type: 'text', content: msg.text.trim() }); + } else if (msg.message?.content && Array.isArray(msg.message.content)) { + // Process each content block + msg.message.content.forEach((block: any) => { + if (block.type === 'text' && block.text?.trim()) { + segments.push({ type: 'text', content: block.text.trim() }); + } else if (block.type === 'thinking') { + const thinkingContent = block.thinking || block.content || block.text; + if (thinkingContent && typeof thinkingContent === 'string' && thinkingContent.trim()) { + segments.push({ type: 'thinking', content: thinkingContent.trim() }); + } + } else if (block.type === 'tool_use') { + const toolCall: ToolCall = { + id: block.id, + name: block.name, + input: block.input, + status: toolResults.has(block.id) ? 'success' : 'pending', + result: toolResults.get(block.id) + }; + segments.push({ type: 'tool_call', tool: toolCall }); + } + }); + } else { + // Fallback for other formats + const textContent = extractTextContent(msg); + if (textContent) { + segments.push({ type: 'text', content: textContent }); + } + } + + // Only add message if it has content + if (segments.length > 0) { + // Check if this is a synthetic error message + const isSyntheticError = msg.message?.model === '' && + segments.some(seg => seg.type === 'text' && + (seg.content.includes('Prompt is too long') || + seg.content.includes('API Error') || + seg.content.includes('error'))); + + transformed.push({ + id: msg.id || `assistant-${i}-${msg.timestamp}`, + role: isSyntheticError ? 'system' : 'assistant', + timestamp: msg.timestamp, + segments, + metadata: { + agent: detectAgent(msg), + model: msg.message?.model, + duration: msg.message?.duration, + tokens: msg.message?.usage ? + (msg.message.usage.input_tokens || 0) + (msg.message.usage.output_tokens || 0) : + undefined, + cost: msg.message?.usage?.cost, + systemSubtype: isSyntheticError ? 'error' : undefined + } + }); + } + + } else if (msg.type === 'system' && msg.subtype === 'init') { + // Include system init messages + transformed.push({ + id: msg.id || `system-init-${i}-${msg.timestamp}`, + role: 'system', + timestamp: msg.timestamp, + segments: [{ + type: 'system_info', + info: { + cwd: msg.cwd, + model: msg.model, + tools: msg.tools, + mcp_servers: msg.mcp_servers, + permissionMode: msg.permissionMode, + session_id: msg.session_id + } + }], + metadata: { + systemSubtype: 'init', + sessionInfo: msg + } + }); + + } else if (msg.type === 'system' && msg.subtype === 'context_compacted') { + // Handle context compaction messages + transformed.push({ + id: msg.id || `context-compacted-${i}-${msg.timestamp}`, + role: 'system', + timestamp: msg.timestamp, + segments: [{ + type: 'text', + content: msg.summary || '' + }, { + type: 'system_info', + info: { + message: msg.message + } + }], + metadata: { + systemSubtype: 'context_compacted' + } + }); + + } else if (msg.type === 'system' && msg.subtype === 'error') { + // Handle error messages from session manager + transformed.push({ + id: msg.id || `error-${i}-${msg.timestamp}`, + role: 'system', + timestamp: msg.timestamp, + segments: [{ + type: 'system_info', + info: { + error: msg.error, + details: msg.details, + message: msg.message + } + }], + metadata: { + systemSubtype: 'error' + } + }); + + } else if (msg.type === 'system' && msg.subtype === 'git_operation') { + // Handle git operation messages + transformed.push({ + id: msg.id || `git-operation-${i}-${msg.timestamp}`, + role: 'system', + timestamp: msg.timestamp, + segments: [{ + type: 'text', + content: msg.message || msg.raw_output || '' + }], + metadata: { + systemSubtype: 'git_operation' + } + }); + + } else if (msg.type === 'system' && msg.subtype === 'git_error') { + // Handle git error messages + transformed.push({ + id: msg.id || `git-error-${i}-${msg.timestamp}`, + role: 'system', + timestamp: msg.timestamp, + segments: [{ + type: 'text', + content: msg.message || msg.raw_output || '' + }], + metadata: { + systemSubtype: 'git_error' + } + }); + + } else if (msg.type === 'result') { + // Handle execution result messages - especially errors + if (msg.is_error && msg.result) { + transformed.push({ + id: msg.id || `error-${i}-${msg.timestamp}`, + role: 'system', + timestamp: msg.timestamp, + segments: [{ + type: 'text', + content: `Error: ${msg.result}` + }], + metadata: { + systemSubtype: 'error', + duration: msg.duration_ms, + cost: msg.total_cost_usd + } + }); + } + // Skip non-error result messages + continue; + } + } + + return transformed; + }; + + + const loadMessages = useCallback(async () => { + // Prevent concurrent loads using ref + if (isLoadingRef.current) return; + isLoadingRef.current = true; + + try { + setError(null); + + // Load conversation messages and JSON messages + const [conversationResponse, outputResponse] = await Promise.all([ + API.sessions.getConversation(sessionId), + API.sessions.getJsonMessages(sessionId) + ]); + + // Combine both sources - conversation messages have the actual user prompts + const userPrompts: RawMessage[] = []; + if (conversationResponse.success && Array.isArray(conversationResponse.data)) { + conversationResponse.data.forEach((msg: any) => { + if (msg.message_type === 'user') { + userPrompts.push({ + type: 'user', + message: { + role: 'user', + content: [{ type: 'text', text: msg.content }] + }, + timestamp: msg.timestamp + }); + } + }); + } + + // Combine user prompts with output messages (filter for JSON messages) + const allMessages = [...userPrompts]; + if (outputResponse.success && outputResponse.data && Array.isArray(outputResponse.data)) { + // JSON messages are already in the correct format from getJsonMessages + allMessages.push(...outputResponse.data); + } + + // Sort by timestamp to get correct order + allMessages.sort((a, b) => { + const timeA = new Date(a.timestamp).getTime(); + const timeB = new Date(b.timestamp).getTime(); + return timeA - timeB; + }); + + + const conversationMessages = transformMessages(allMessages); + setMessages(conversationMessages); + } catch (err) { + console.error('Failed to load messages:', err); + setError('Failed to load conversation history'); + } finally { + setLoading(false); + isLoadingRef.current = false; + } + }, [sessionId]); + + // Listen for real-time output updates - debounced to prevent performance issues + useEffect(() => { + let debounceTimer: NodeJS.Timeout; + + const handleOutputAvailable = (event: CustomEvent<{ sessionId: string }>) => { + if (event.detail.sessionId === sessionId) { + // Debounce message reloading to prevent excessive re-renders + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + loadMessages(); + }, 500); // Wait 500ms after last event + } + }; + + window.addEventListener('session-output-available', handleOutputAvailable as any); + + return () => { + clearTimeout(debounceTimer); + window.removeEventListener('session-output-available', handleOutputAvailable as any); + }; + }, [sessionId, loadMessages]); + + // Initial load + useEffect(() => { + if (!sessionId) return; + loadMessages(); + }, [sessionId, loadMessages]); + + // Track if user is at bottom - set up once when container is available + useEffect(() => { + let cleanup: (() => void) | undefined; + + // Use a small delay to ensure DOM is ready + const timer = setTimeout(() => { + const container = scrollContainerRef.current; + if (!container) return; + + const checkIfAtBottom = () => { + const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 50; + wasAtBottomRef.current = isAtBottom; + }; + + // Check initial position + checkIfAtBottom(); + + container.addEventListener('scroll', checkIfAtBottom); + + // Store cleanup function + cleanup = () => container.removeEventListener('scroll', checkIfAtBottom); + }, 100); + + return () => { + clearTimeout(timer); + if (cleanup) cleanup(); + }; + }, []); // Empty array - set up only once + + // Auto-scroll to bottom when messages change or view loads + useEffect(() => { + if (messagesEndRef.current && !loading) { + // Scroll if we were at the bottom before the update + if (wasAtBottomRef.current) { + // Use requestAnimationFrame to ensure DOM has updated + requestAnimationFrame(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' as ScrollBehavior }); + // Don't set wasAtBottomRef here - let the scroll event handler determine actual position + }); + } + } + }, [messages, loading]); + + // Handle scroll events to show/hide scroll button + useEffect(() => { + const container = scrollContainerRef.current; + if (!container) return; + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = container; + // Show button if scrolled up more than one viewport height + const scrolledUp = scrollHeight - scrollTop - clientHeight; + setShowScrollButton(scrolledUp > clientHeight); + }; + + container.addEventListener('scroll', handleScroll); + // Check initial state + handleScroll(); + + return () => container.removeEventListener('scroll', handleScroll); + }, [messages]); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + const toggleMessageCollapse = (messageId: string) => { + setCollapsedMessages(prev => { + const next = new Set(prev); + if (next.has(messageId)) { + next.delete(messageId); + } else { + next.add(messageId); + } + return next; + }); + }; + + const toggleToolExpand = (toolId: string) => { + setExpandedTools(prev => { + const next = new Set(prev); + if (next.has(toolId)) { + next.delete(toolId); + } else { + next.add(toolId); + } + return next; + }); + }; + + + // Render a tool call segment + const renderToolCall = (tool: ToolCall) => { + const isExpanded = !settings.collapseTools || expandedTools.has(tool.id); + + return ( +
+ + + {isExpanded && ( +
+ {/* Tool Parameters */} + {tool.input && Object.keys(tool.input).length > 0 && ( +
+
Parameters:
+ {formatToolInput(tool.name, tool.input)} +
+ )} + + {/* Tool Result */} + {tool.result && ( +
+
+ {tool.result.isError ? 'Error:' : 'Result:'} +
+
+ {formatToolResult(tool.name, tool.result.content)} +
+
+ )} + + {/* Pending state */} + {tool.status === 'pending' && ( +
Waiting for result...
+ )} +
+ )} +
+ ); + }; + + // Format tool input for display + const formatToolInput = (toolName: string, input: any): React.ReactNode => { + switch (toolName) { + case 'Read': + return ( +
+ {input.file_path && ( +
+ File: + {input.file_path} +
+ )} + {input.offset &&
Lines: {input.offset}-{input.offset + (input.limit || 2000)}
} +
+ ); + + case 'Edit': + case 'MultiEdit': + return ( +
+ {input.file_path &&
File: {input.file_path}
} + {toolName === 'MultiEdit' && input.edits && ( +
{input.edits.length} changes
+ )} +
+ ); + + case 'Write': + return ( +
+ {input.file_path &&
File: {input.file_path}
} + {input.content && ( +
{input.content.split('\n').length} lines
+ )} +
+ ); + + case 'Bash': + return ( +
+ $ {input.command} +
+ ); + + case 'Grep': + return ( +
+
Pattern: "{input.pattern}"
+ {input.path &&
Path: {input.path}
} + {input.glob &&
Files: {input.glob}
} +
+ ); + + case 'TodoWrite': + return ( +
+ {input.todos && input.todos.map((todo: any, idx: number) => { + const icon = todo.status === 'completed' ? '✓' : + todo.status === 'in_progress' ? '→' : '○'; + const color = todo.status === 'completed' ? 'text-status-success' : + todo.status === 'in_progress' ? 'text-status-warning' : 'text-text-tertiary'; + return ( +
+ {icon} {todo.content} +
+ ); + })} +
+ ); + + default: + // Compact display for unknown tools + return ( +
+            {JSON.stringify(input, null, 2)}
+          
+ ); + } + }; + + // Format tool result for display + const formatToolResult = (_toolName: string, result: string): React.ReactNode => { + if (!result) { + return
No result
; + } + + try { + // Check if result is JSON + const parsed = JSON.parse(result); + + // Handle image reads + if (Array.isArray(parsed) && parsed[0]?.type === 'image') { + return ( +
+ [Image displayed to assistant] +
+ ); + } + + // For other JSON results, pretty print compactly + return ( +
+          {JSON.stringify(parsed, null, 2)}
+        
+ ); + } catch { + // Not JSON, display as text + if (result.length > 300) { + return ( +
+ + {result.substring(0, 100)}... (click to expand) + +
{result}
+
+ ); + } + + return
{result}
; + } + }; + + // Render a complete message + const renderMessage = (message: ConversationMessage, index: number, userMessageIndex?: number) => { + const isCollapsed = collapsedMessages.has(message.id); + const isUser = message.role === 'user'; + const isSystem = message.role === 'system'; + const hasTextContent = message.segments.some(seg => seg.type === 'text'); + const textContent = message.segments + .filter(seg => seg.type === 'text') + .map(seg => seg.type === 'text' ? seg.content : '') + .join('\n\n'); + + // Check if message has tool calls or thinking + const hasToolCalls = message.segments.some(seg => seg.type === 'tool_call'); + const hasThinking = message.segments.some(seg => seg.type === 'thinking'); + + // Determine if we need extra spacing before this message + const prevMessage = index > 0 ? filteredMessages[index - 1] : null; + const needsExtraSpacing = prevMessage && ( + (prevMessage.role !== message.role) || + (hasThinking && !prevMessage.segments.some(seg => seg.type === 'thinking')) + ); + + // Special rendering for system messages + if (isSystem) { + if (message.metadata?.systemSubtype === 'init') { + const info = message.segments.find(seg => seg.type === 'system_info'); + if (info?.type === 'system_info') { + return ( +
+
+
+ +
+
+
+ Session Started + + {formatDistanceToNow(parseTimestamp(message.timestamp))} + +
+
+
Model: {info.info.model}
+
Working Directory: {info.info.cwd}
+
+ Tools: {info.info.tools?.length || 0} available +
+
+
+
+
+ ); + } + } else if (message.metadata?.systemSubtype === 'error') { + // Render error messages - get error info from system_info segment + const errorInfo = message.segments.find(seg => seg.type === 'system_info')?.info; + const errorMessage = errorInfo?.message || textContent; + const errorTitle = errorInfo?.error || 'Session Error'; + + return ( +
+
+
+ +
+
+
+ {errorTitle} + + {formatDistanceToNow(parseTimestamp(message.timestamp))} + + {message.metadata?.duration && ( + + · {(message.metadata.duration / 1000).toFixed(1)}s + + )} +
+
+ {errorMessage} +
+
+
+
+ ); + } else if (message.metadata?.systemSubtype === 'context_compacted') { + // Render context compaction messages + // Get the message info from the system_info segment + const infoSegment = message.segments.find(seg => seg.type === 'system_info'); + const helpMessage = infoSegment?.type === 'system_info' ? infoSegment.info.message : + 'Context has been compacted. You can continue chatting - your next message will automatically include the context summary above.'; + + return ( +
+
+
+ + + +
+
+
+ Context Compacted + + {formatDistanceToNow(parseTimestamp(message.timestamp))} + +
+ + {/* Summary content */} +
+
+ {textContent} +
+
+ + {/* Clear instruction message */} +
+ +
+ Ready to continue! {helpMessage} +
+
+
+
+
+ ); + } else if (message.metadata?.systemSubtype === 'git_operation') { + // Render git operation messages + const rawOutput = message.segments.find(seg => seg.type === 'text')?.content || textContent; + const isSuccess = rawOutput.includes('✓') || rawOutput.includes('Successfully'); + + // Parse the git operation message for better formatting + const lines = rawOutput.split('\n'); + const mainMessage = lines.filter(line => !line.includes('🔄 GIT OPERATION') && line.trim()).join('\n'); + + return ( +
+
+
+ + + +
+
+
+ + 🔄 Git Operation + + + {formatDistanceToNow(parseTimestamp(message.timestamp))} + +
+
+ {mainMessage.split('\n').map((line, idx) => { + const trimmedLine = line.trim(); + if (!trimmedLine) return null; + + if (trimmedLine.startsWith('✓')) { + return ( +
+ + {trimmedLine.substring(1).trim()} +
+ ); + } else if (trimmedLine.startsWith('Commit message:')) { + return ( +
+ {trimmedLine} +
+ ); + } else if (trimmedLine.includes('Git output:')) { + return ( +
+ {trimmedLine} +
+ ); + } else { + return ( +
+ {trimmedLine} +
+ ); + } + })} +
+
+
+
+ ); + } else if (message.metadata?.systemSubtype === 'git_error') { + // Render git error messages + const rawOutput = message.segments.find(seg => seg.type === 'text')?.content || textContent; + + // Parse the git error message for better formatting + const lines = rawOutput.split('\n'); + const mainMessage = lines.filter(line => line.trim()).join('\n'); + + return ( +
+
+
+ +
+
+
+ Git Operation Failed + + {formatDistanceToNow(parseTimestamp(message.timestamp))} + +
+
+ {mainMessage.split('\n').map((line, idx) => { + const trimmedLine = line.trim(); + if (!trimmedLine) return null; + + if (trimmedLine.startsWith('✗')) { + return ( +
+ + {trimmedLine.substring(1).trim()} +
+ ); + } else if (trimmedLine.includes('Git output:')) { + return ( +
+ {trimmedLine} +
+ ); + } else { + return ( +
+ {trimmedLine} +
+ ); + } + })} +
+
+
+
+ ); + } + } + + return ( +
{ + if (el) userMessageRefs.current.set(userMessageIndex, el); + } : undefined} + className={` + rounded-lg transition-all + ${isUser ? 'bg-surface-secondary' : hasThinking ? 'bg-surface-primary/50' : 'bg-surface-primary'} + ${hasToolCalls ? 'bg-surface-tertiary/30' : ''} + ${settings.compactMode ? 'p-3' : 'p-4'} + ${needsExtraSpacing ? 'mt-4' : ''} + `} + > + {/* Message Header */} +
+
+ {isUser ? : } +
+
+ + {isUser ? 'You' : getAgentName(message.metadata?.agent)} + + + {formatDistanceToNow(parseTimestamp(message.timestamp))} + + {message.metadata?.duration && ( + + · {(message.metadata.duration / 1000).toFixed(1)}s + + )} +
+ {hasTextContent && textContent.length > 200 && ( + + )} +
+ + {/* Message Content */} +
+ {/* Thinking segments */} + {settings.showThinking && message.segments + .filter(seg => seg.type === 'thinking') + .map((seg, idx) => { + if (seg.type === 'thinking') { + return ( +
+
+
+
+ +
+
+
+ ); + } + return null; + }) + } + + {/* Text segments */} + {hasTextContent && ( +
+ {isUser ? ( +
{textContent}
+ ) : ( +
+ +
+ )} + {isCollapsed && ( +
+ )} +
+ )} + + {/* Tool calls */} + {settings.showToolCalls && message.segments + .filter(seg => seg.type === 'tool_call') + .map((seg, idx) => { + if (seg.type === 'tool_call') { + return ( +
+ {renderToolCall(seg.tool)} +
+ ); + } + return null; + }) + } + +
+
+ ); + }; + + const getAgentName = (agent?: string) => { + switch (agent) { + case 'claude': return 'Claude'; + case 'gpt-4': + case 'openai': return 'GPT-4'; + case 'gemini': + case 'google': return 'Gemini'; + default: return 'Assistant'; + } + }; + + // Check if we're waiting for Claude's response + const isWaitingForResponse = useMemo(() => { + // Always show placeholder if session is actively running + if (sessionStatus === 'running') { + return true; + } + + // Also show if waiting and last message is from user + if (sessionStatus === 'waiting' && messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + return lastMessage.role === 'user'; + } + + return false; + }, [messages, sessionStatus]); + + // Filter messages based on settings + const filteredMessages = useMemo(() => { + if (settings.showSessionInit) { + return messages; + } + // Filter out session init messages + return messages.filter(msg => !(msg.role === 'system' && msg.metadata?.systemSubtype === 'init')); + }, [messages, settings.showSessionInit]); + + // Memoize the rendered messages to prevent unnecessary re-renders + const renderedMessages = useMemo(() => { + let userMessageIndex = 0; + return filteredMessages.map((msg, idx) => { + const isUser = msg.role === 'user'; + const element = renderMessage(msg, idx, isUser ? userMessageIndex : undefined); + if (isUser) userMessageIndex++; + return element; + }); + }, [filteredMessages, collapsedMessages, expandedTools, settings]); + + if (loading) { + return ( +
+
Loading conversation...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+
+ ); + } + + return ( +
+ {/* Settings panel is now rendered in SessionView to avoid duplication */} + + {/* Messages */} +
+
+ {filteredMessages.length === 0 && !isWaitingForResponse ? ( +
+ No messages to display +
+ ) : ( +
+ {renderedMessages} + {isWaitingForResponse && ( + filteredMessages.length === 0 || + (filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1].role === 'user') ? ( + + ) : ( + + ) + )} +
+
+ )} +
+ + {/* Scroll to bottom button - centered above input */} + {showScrollButton && ( +
+ +
+ )} +
+
+ ); +}); + +RichOutputView.displayName = 'RichOutputView'; \ No newline at end of file diff --git a/frontend/src/components/session/RichOutputView.tsx b/frontend/src/components/session/RichOutputView.tsx index 67ba0510..99c6421d 100644 --- a/frontend/src/components/session/RichOutputView.tsx +++ b/frontend/src/components/session/RichOutputView.tsx @@ -413,6 +413,8 @@ export const RichOutputView = React.forwardRef<{ scrollToPrompt: (promptIndex: n if (isLoadingRef.current) return; isLoadingRef.current = true; + console.log('[RichOutputView] Loading messages for session:', sessionId); + try { setError(null); @@ -422,6 +424,9 @@ export const RichOutputView = React.forwardRef<{ scrollToPrompt: (promptIndex: n API.sessions.getJsonMessages(sessionId) ]); + console.log('[RichOutputView] Conversation response:', conversationResponse); + console.log('[RichOutputView] Output response:', outputResponse); + // Combine both sources - conversation messages have the actual user prompts const userPrompts: RawMessage[] = []; if (conversationResponse.success && Array.isArray(conversationResponse.data)) { diff --git a/main/src/ipc/session.ts b/main/src/ipc/session.ts index 66984c86..5bb35642 100644 --- a/main/src/ipc/session.ts +++ b/main/src/ipc/session.ts @@ -6,6 +6,7 @@ import type { AppServices } from './types'; import type { CreateSessionRequest } from '../types/session'; import { getCrystalSubdirectory } from '../utils/crystalDirectory'; import { convertDbFolderToFolder } from './folders'; +import stripAnsi from 'strip-ansi'; export function registerSessionHandlers(ipcMain: IpcMain, services: AppServices): void { const { @@ -285,8 +286,14 @@ export function registerSessionHandlers(ipcMain: IpcMain, services: AppServices) } }); + // Store last user inputs per session to filter out echoes (with flag if already filtered) + const lastUserInputs = new Map(); + ipcMain.handle('sessions:input', async (_event, sessionId: string, input: string) => { try { + // Store the raw input to filter out Claude's echo later (reset filtered flag) + lastUserInputs.set(sessionId, { text: input.trim(), filtered: false }); + // Update session status back to running when user sends input const currentSession = await sessionManager.getSession(sessionId); if (currentSession && currentSession.status === 'waiting') { @@ -630,12 +637,52 @@ export function registerSessionHandlers(ipcMain: IpcMain, services: AppServices) data.includes('Aborted rebase successfully'); }; - // Filter to JSON messages, error messages, and git operation stdout/stderr messages - const jsonMessages = outputs + // Group consecutive stdout messages that come within 100ms of each other + const groupedOutputs: any[] = []; + let currentGroup: any = null; + + for (const output of outputs) { + if (output.type === 'stdout' && !isGitOperation(output.data)) { + // Check if we should combine with previous stdout + if (currentGroup && currentGroup.type === 'stdout' && + (new Date(output.timestamp).getTime() - new Date(currentGroup.timestamp).getTime()) < 100) { + // Append to current group + // Check if the previous data ends with a sentence-ending punctuation + const prevEndsComplete = currentGroup.data.match(/[.!?:]\s*$/); + const currStartsCapital = output.data.match(/^[A-Z]/); + + if (prevEndsComplete || currStartsCapital) { + // Previous line is complete or current starts with capital - add newline + currentGroup.data += '\n' + output.data; + } else { + // Lines seem to continue - add space + currentGroup.data += ' ' + output.data; + } + currentGroup.timestamp = output.timestamp; // Update to latest timestamp + } else { + // Start new group + if (currentGroup) groupedOutputs.push(currentGroup); + currentGroup = { ...output }; + } + } else { + // Not stdout or is git operation - flush current group and add this output + if (currentGroup) { + groupedOutputs.push(currentGroup); + currentGroup = null; + } + groupedOutputs.push(output); + } + } + // Don't forget the last group + if (currentGroup) groupedOutputs.push(currentGroup); + + // Filter to JSON messages, error messages, and ALL stdout/stderr messages + const jsonMessages = groupedOutputs .filter(output => output.type === 'json' || output.type === 'error' || - ((output.type === 'stdout' || output.type === 'stderr') && isGitOperation(output.data)) + output.type === 'stdout' || // Show ALL stdout, not just git operations + output.type === 'stderr' // Show ALL stderr too ) .map(output => { if (output.type === 'error') { @@ -649,16 +696,96 @@ export function registerSessionHandlers(ipcMain: IpcMain, services: AppServices) message: `${output.data.error}${output.data.details ? '\n\n' + output.data.details : ''}` }; } else if (output.type === 'stdout' || output.type === 'stderr') { - // Transform git operation stdout/stderr to system messages that RichOutputView can display + // Check if this is a git operation or Claude's response + const isGitOp = isGitOperation(output.data); const isError = output.type === 'stderr' || output.data.includes('failed:') || output.data.includes('✗'); - return { - type: 'system', - subtype: isError ? 'git_error' : 'git_operation', - timestamp: output.timestamp.toISOString(), - message: output.data, - // Add raw data for processing - raw_output: output.data - }; + + if (isGitOp) { + // Git operation - treat as system message + return { + type: 'system', + subtype: isError ? 'git_error' : 'git_operation', + timestamp: output.timestamp.toISOString(), + message: output.data, + // Add raw data for processing + raw_output: output.data + }; + } else { + // Claude's plain text response - treat as assistant message + // Since we're spawning Claude directly (no shell), we shouldn't get shell prompts + // But --verbose flag might still cause echoing + + // Strip ANSI codes and terminal control sequences + let cleanText = stripAnsi(output.data).trim(); + + // Skip empty lines + if (!cleanText) return null; + + // Skip user input echoes (from --verbose flag) + if (cleanText.startsWith('> ')) return null; + + // Skip if this exactly matches what the user just typed (Claude echoing) + const lastInput = lastUserInputs.get(sessionId); + if (lastInput && !lastInput.filtered && cleanText === lastInput.text) { + console.log(`[FILTER] Skipping Claude's echo of user input: "${cleanText}"`); + lastInput.filtered = true; + return null; + } + + // Skip separator lines + if (cleanText.match(/^[─\-_═]{10,}$/)) return null; + + // Skip Claude's internal logging (if any) + if (cleanText.includes('👤 USER PROMPT')) return null; + if (cleanText.includes('SESSION SUMMARY')) return null; + if (cleanText.includes('No commits were made')) return null; + + // Clean up any MCP server launch commands that might still appear + // Sometimes it's "pm exec @..." and sometimes just "exec @..." + if (cleanText.includes('exec @')) { + // More precise MCP command removal - only remove the actual MCP commands + // Match the full package names to avoid removing parts of actual messages + cleanText = cleanText.replace(/pm exec @modelcontextprotocol\/[a-zA-Z0-9\-]+\s*/g, ''); + cleanText = cleanText.replace(/exec @modelcontextprotocol\/[a-zA-Z0-9\-]+\s*/g, ''); + cleanText = cleanText.replace(/pm exec @playwright\/[a-zA-Z0-9\-@]+\s*/g, ''); + cleanText = cleanText.replace(/exec @playwright\/[a-zA-Z0-9\-@]+\s*/g, ''); + cleanText = cleanText.replace(/pm exec @upstash\/[a-zA-Z0-9\-]+\s*/g, ''); + cleanText = cleanText.replace(/exec @upstash\/[a-zA-Z0-9\-]+\s*/g, ''); + cleanText = cleanText.replace(/pm exec @21st-dev\/[a-zA-Z0-9\-]+\s*/g, ''); + cleanText = cleanText.replace(/exec @21st-dev\/[a-zA-Z0-9\-]+\s*/g, ''); + + // Remove any leading non-printable characters or Unicode boxes + cleanText = cleanText.replace(/^[\x00-\x1F\x7F-\x9F\u2000-\u200F\uFEFF]+/, ''); + cleanText = cleanText.trim(); + } + + // If we still see Windows paths, clean them + if (cleanText.includes(':\\Windows\\')) { + console.warn(`[WARNING] Still seeing Windows paths despite direct spawning`); + cleanText = cleanText.replace(/[A-Z]?:\\Windows\\System32\\cmd\.exe\s*/g, ''); + cleanText = cleanText.trim(); + } + + // Final cleanup - remove any remaining box characters or non-printable chars at the start + cleanText = cleanText.replace(/^[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F\u2000-\u200F\uFEFF]+/, ''); + + // Skip if we have nothing left + if (!cleanText || cleanText.length < 2) return null; + + console.log(`[CLAUDE OUTPUT]: "${cleanText}"`); + + + return { + type: 'assistant', + timestamp: output.timestamp.toISOString(), + message: { + content: [{ + type: 'text', + text: cleanText + }] + } + }; + } } else { // Regular JSON messages return { @@ -666,7 +793,8 @@ export function registerSessionHandlers(ipcMain: IpcMain, services: AppServices) timestamp: output.timestamp.toISOString() }; } - }); + }) + .filter(message => message !== null); // Remove null entries (empty messages) console.log(`[IPC] Found ${jsonMessages.length} messages (including git operations) for session ${sessionId}`); return { success: true, data: jsonMessages }; diff --git a/main/src/services/claudeCodeManager.ts b/main/src/services/claudeCodeManager.ts index 475ed7db..ffbc4c1c 100644 --- a/main/src/services/claudeCodeManager.ts +++ b/main/src/services/claudeCodeManager.ts @@ -162,7 +162,8 @@ export class ClaudeCodeManager extends EventEmitter { } // Build the command arguments - const args = ['--verbose', '--output-format', 'stream-json']; + // Need --verbose for Claude to output anything in plain text mode + const args: string[] = ['--verbose']; // Add model argument if specified if (model) { @@ -607,6 +608,7 @@ export class ClaudeCodeManager extends EventEmitter { try { console.log(`[ClaudeManager] Spawning Claude process (attempt ${spawnAttempt + 1})...`); console.log(`[ClaudeManager] Command: ${claudeCommand}`); + console.log(`[ClaudeManager] Arguments: ${args.join(' ')}`); console.log(`[ClaudeManager] Working directory: ${worktreePath}`); console.log(`[ClaudeManager] PATH entries: ${pathWithNode.split(pathSeparator).length}`); const startTime = Date.now(); @@ -619,6 +621,8 @@ export class ClaudeCodeManager extends EventEmitter { if (spawnAttempt === 0) { // First attempt: normal spawn + // DON'T specify a shell - let node-pty run Claude directly + // This avoids shell prompts polluting the output ptyProcess = pty.spawn(claudeCommand, args, { name: 'xterm-color', cols: 80, @@ -752,41 +756,150 @@ export class ClaudeCodeManager extends EventEmitter { lastOutput += data; buffer += data; - // Process complete JSON lines - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; // Keep incomplete line in buffer + // Debug: Log raw data to see what Claude is sending + console.log(`\x1b[36m${'='.repeat(80)}\x1b[0m`); + console.log(`\x1b[36m[ClaudeManager DEBUG] Raw data from Claude (first 500 chars):\x1b[0m`); + console.log(`\x1b[33m${data.substring(0, 500).replace(/\n/g, '\\n').replace(/\r/g, '\\r')}\x1b[0m`); + console.log(`\x1b[36m${'='.repeat(80)}\x1b[0m`); - for (const line of lines) { - if (line.trim()) { - try { - const jsonMessage = JSON.parse(line.trim()); - this.logger?.verbose(`JSON message from session ${sessionId}: ${JSON.stringify(jsonMessage)}`); + // Claude sends streaming JSON that can be split across chunks + // We need to detect complete JSON objects by counting braces + + // Process the buffer to extract complete JSON objects + while (buffer.length > 0) { + // Skip leading whitespace + const trimStart = buffer.search(/\S/); + if (trimStart > 0) { + buffer = buffer.substring(trimStart); + } else if (trimStart === -1) { + // Only whitespace in buffer + buffer = ''; + break; + } + + // Check if buffer starts with JSON + if (buffer[0] === '{') { + // Count braces to find the end of the JSON object + let depth = 0; + let inString = false; + let escape = false; + let jsonEnd = -1; + + for (let i = 0; i < buffer.length; i++) { + const char = buffer[i]; - // Emit JSON message only - terminal formatting will be done on the fly - this.emit('output', { - sessionId, - type: 'json', - data: jsonMessage, - timestamp: new Date() - }); - } catch (error) { - // If not valid JSON, treat as regular output - this.logger?.verbose(`Raw output from session ${sessionId}: ${line.substring(0, 200)}`); + if (escape) { + escape = false; + continue; + } + + if (char === '\\') { + escape = true; + continue; + } + + if (char === '"') { + inString = !inString; + continue; + } + + if (!inString) { + if (char === '{') { + depth++; + } else if (char === '}') { + depth--; + if (depth === 0) { + jsonEnd = i + 1; + break; + } + } + } + } + + if (jsonEnd > 0) { + // Found complete JSON object + const jsonStr = buffer.substring(0, jsonEnd); + buffer = buffer.substring(jsonEnd); - // Check if this looks like an error message - const isError = line.includes('ERROR') || - line.includes('Error:') || - line.includes('error:') || - line.includes('Command failed:') || - line.includes('aborted') || - line.includes('fatal:'); + try { + const jsonMessage = JSON.parse(jsonStr); + + console.log(`\x1b[32m✅ [ClaudeManager DEBUG] Successfully parsed JSON: type=${jsonMessage.type || jsonMessage.object || jsonMessage.role}\x1b[0m`); + + // Special highlight for assistant messages + if (jsonMessage.type === 'assistant' || jsonMessage.role === 'assistant') { + console.log(`\x1b[35m\x1b[1m🤖 CLAUDE RESPONSE DETECTED!\x1b[0m`); + console.log(`\x1b[35mContent: ${JSON.stringify(jsonMessage.content || jsonMessage.message || jsonMessage).substring(0, 300)}\x1b[0m`); + } + + // Check for streaming chunks + if (jsonMessage.object === 'chat.completion.chunk') { + console.log(`\x1b[34m📨 STREAMING CHUNK DETECTED! Delta: ${JSON.stringify(jsonMessage.choices?.[0]?.delta).substring(0, 200)}\x1b[0m`); + } + + // Emit JSON message + this.emit('output', { + sessionId, + type: 'json', + data: jsonMessage, + timestamp: new Date() + }); + } catch (error) { + console.log(`\x1b[31m❌ [ClaudeManager DEBUG] Failed to parse JSON: ${error}\x1b[0m`); + console.log(`\x1b[31mJSON string (first 200 chars): ${jsonStr.substring(0, 200)}\x1b[0m`); + + // Emit as stdout if parse fails + this.emit('output', { + sessionId, + type: 'stdout', + data: jsonStr + '\n', + timestamp: new Date() + }); + } + } else { + // Incomplete JSON, wait for more data + console.log(`\x1b[33m⏳ [ClaudeManager DEBUG] Incomplete JSON, buffering... (depth=${depth}, buffer size=${buffer.length})\x1b[0m`); + break; + } + } else { + // Not JSON, look for next JSON object or newline + const nextJson = buffer.indexOf('{'); + const nextNewline = buffer.indexOf('\n'); + + let splitPoint = -1; + if (nextJson >= 0 && (nextNewline === -1 || nextJson < nextNewline)) { + splitPoint = nextJson; + } else if (nextNewline >= 0) { + splitPoint = nextNewline + 1; + } + + if (splitPoint > 0) { + const text = buffer.substring(0, splitPoint); + buffer = buffer.substring(splitPoint); - this.emit('output', { - sessionId, - type: isError ? 'stderr' : 'stdout', - data: line + '\n', - timestamp: new Date() - }); + if (text.trim()) { + // Emit non-JSON text as stdout + this.emit('output', { + sessionId, + type: 'stdout', + data: text, + timestamp: new Date() + }); + } + } else { + // No more JSON or newlines, emit rest as stdout if it's getting long + if (buffer.length > 1000) { + this.emit('output', { + sessionId, + type: 'stdout', + data: buffer, + timestamp: new Date() + }); + buffer = ''; + } else { + // Keep buffering + break; + } } } } @@ -980,6 +1093,8 @@ export class ClaudeCodeManager extends EventEmitter { throw new Error(`No Claude Code process found for session ${sessionId}`); } + console.log(`\x1b[33m📝 [ClaudeManager] Sending input to session ${sessionId}:\x1b[0m`); + console.log(`\x1b[33m${input.substring(0, 200)}${input.length > 200 ? '...' : ''}\x1b[0m`); claudeProcess.process.write(input); } From 0b7f88b1c265420277509736bfc3079a2d36df4b Mon Sep 17 00:00:00 2001 From: Sallvainian Date: Wed, 6 Aug 2025 07:11:12 -0400 Subject: [PATCH 09/14] Clean up debugging code and simplify filtering - Removed debug console.log statements - Simplified MCP command filtering to single regex pattern - Kept message grouping and echo filtering as they're needed --- main/src/ipc/session.ts | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/main/src/ipc/session.ts b/main/src/ipc/session.ts index 5bb35642..3a8862e6 100644 --- a/main/src/ipc/session.ts +++ b/main/src/ipc/session.ts @@ -727,7 +727,6 @@ export function registerSessionHandlers(ipcMain: IpcMain, services: AppServices) // Skip if this exactly matches what the user just typed (Claude echoing) const lastInput = lastUserInputs.get(sessionId); if (lastInput && !lastInput.filtered && cleanText === lastInput.text) { - console.log(`[FILTER] Skipping Claude's echo of user input: "${cleanText}"`); lastInput.filtered = true; return null; } @@ -741,27 +740,14 @@ export function registerSessionHandlers(ipcMain: IpcMain, services: AppServices) if (cleanText.includes('No commits were made')) return null; // Clean up any MCP server launch commands that might still appear - // Sometimes it's "pm exec @..." and sometimes just "exec @..." if (cleanText.includes('exec @')) { - // More precise MCP command removal - only remove the actual MCP commands - // Match the full package names to avoid removing parts of actual messages - cleanText = cleanText.replace(/pm exec @modelcontextprotocol\/[a-zA-Z0-9\-]+\s*/g, ''); - cleanText = cleanText.replace(/exec @modelcontextprotocol\/[a-zA-Z0-9\-]+\s*/g, ''); - cleanText = cleanText.replace(/pm exec @playwright\/[a-zA-Z0-9\-@]+\s*/g, ''); - cleanText = cleanText.replace(/exec @playwright\/[a-zA-Z0-9\-@]+\s*/g, ''); - cleanText = cleanText.replace(/pm exec @upstash\/[a-zA-Z0-9\-]+\s*/g, ''); - cleanText = cleanText.replace(/exec @upstash\/[a-zA-Z0-9\-]+\s*/g, ''); - cleanText = cleanText.replace(/pm exec @21st-dev\/[a-zA-Z0-9\-]+\s*/g, ''); - cleanText = cleanText.replace(/exec @21st-dev\/[a-zA-Z0-9\-]+\s*/g, ''); - - // Remove any leading non-printable characters or Unicode boxes - cleanText = cleanText.replace(/^[\x00-\x1F\x7F-\x9F\u2000-\u200F\uFEFF]+/, ''); + // Simple pattern to remove all MCP commands + cleanText = cleanText.replace(/\b(pm )?exec @[a-zA-Z0-9\-\/]+(@[a-zA-Z0-9\-\.]+)?\s*/g, ''); cleanText = cleanText.trim(); } // If we still see Windows paths, clean them if (cleanText.includes(':\\Windows\\')) { - console.warn(`[WARNING] Still seeing Windows paths despite direct spawning`); cleanText = cleanText.replace(/[A-Z]?:\\Windows\\System32\\cmd\.exe\s*/g, ''); cleanText = cleanText.trim(); } @@ -772,9 +758,6 @@ export function registerSessionHandlers(ipcMain: IpcMain, services: AppServices) // Skip if we have nothing left if (!cleanText || cleanText.length < 2) return null; - console.log(`[CLAUDE OUTPUT]: "${cleanText}"`); - - return { type: 'assistant', timestamp: output.timestamp.toISOString(), From c0b596a2faf44793bc3b9fb38de14d03c716dcb5 Mon Sep 17 00:00:00 2001 From: Sallvainian Date: Wed, 6 Aug 2025 13:22:14 -0400 Subject: [PATCH 10/14] Fix git commit failures in worktrees by writing temp files to worktree root The issue was that Crystal was trying to write temporary commit message files to .git/FILENAME, but in git worktrees, .git is a file (not a directory) that contains a pointer to the actual git directory. Changes: - commitManager.ts: Write COMMIT_MSG_TEMP to worktree root instead of .git/ - worktreeManager.ts: Write SQUASH_MSG to worktree root instead of .git/ This fixes the ENOENT error when Crystal tries to create commits in worktrees. --- docs/#/brainstorm.md | 73 ++++++++++++++++++++++++++++ main/src/services/commitManager.ts | 18 +++++-- main/src/services/worktreeManager.ts | 18 +++++-- 3 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 docs/#/brainstorm.md diff --git a/docs/#/brainstorm.md b/docs/#/brainstorm.md new file mode 100644 index 00000000..b8b41ee2 --- /dev/null +++ b/docs/#/brainstorm.md @@ -0,0 +1,73 @@ +# Brainstorm: Crystal Git Commit Failure Issue + +## Session: 2024-12-20 11:45:00 + +### Phase 1: Initial Analysis & Expert Role + +#### Expert Role +As a **Senior DevOps Engineer & Git Internals Specialist** with 12+ years of experience in version control systems, Node.js file systems, and Electron application development, I bring expertise in: +- Git worktree management and internal operations +- Cross-platform file system handling (Windows/Unix) +- Node.js fs module and path resolution +- Electron app sandboxing and file permissions +- Shell command execution in different environments + +#### Initial Assessment +**Core Concept**: Crystal is failing to create git commits through Claude Code because it cannot write a temporary commit message file to the expected location in a git worktree. + +#### Sequential Analysis +- **Layer 1 - Surface Understanding**: The app tries to write `COMMIT_MSG_TEMP` to a `.git` folder but fails with ENOENT (file not found) +- **Layer 2 - Underlying Assumptions**: The code assumes `.git` is a regular directory (but in worktrees it's a file pointing to the actual git directory) +- **Layer 3 - Root Challenge**: Misunderstanding of git worktree structure where `.git` is a file, not a directory + +The error path shows `worktrees/JiraSetup.git/COMMIT_MSG_TEMP` which is suspicious - it should be `worktrees/JiraSetup/.git/COMMIT_MSG_TEMP` or more likely, the temp file shouldn't be in `.git` at all. + +#### Critical Questions Before Proceeding + +**Must Answer**: +1. **Git Worktree Structure**: Is Crystal creating worktrees with names like `JiraSetup.git` (with .git extension) or is this a path construction error? +2. **Code Location**: Which file in Crystal contains the commit logic that's constructing this path? (worktreeManager.ts, commitManager.ts, or elsewhere?) +3. **Frequency**: Does this happen with ALL commits in Crystal, or only in specific scenarios (squash commits, checkpoint commits, normal commits)? + +**Would Help to Know**: +1. Has this ever worked before, or is this a new issue after recent changes? +2. Are you using Crystal on Windows exclusively, or have you tested on other platforms? +3. What version of git is installed on the system? + +**Red Flags Requiring Clarification**: +- **Path Construction Error**: The path `worktrees/JiraSetup.git/COMMIT_MSG_TEMP` suggests the worktree folder itself has `.git` extension, which is non-standard +- **Missing Directory Check**: The code appears to write to `.git` directory without checking if it exists or is actually a directory +- **Potential Race Condition**: The temp file creation might be happening before the worktree is fully initialized + +### Phase 2: Research & Root Cause Analysis + +#### Root Cause Identified +The issue is in both `commitManager.ts` and `worktreeManager.ts` where they try to write temporary commit message files to `.git/FILENAME`: + +```typescript +// commitManager.ts line 99 +const tempFile = join(worktreePath, '.git', 'COMMIT_MSG_TEMP'); + +// worktreeManager.ts line 489 +const tempFile = join(worktreePath, '.git', 'SQUASH_MSG'); +``` + +**Why This Fails in Worktrees**: +- In regular git repos: `.git` is a directory containing all git metadata +- In git worktrees: `.git` is a FILE containing a pointer to the actual git directory +- Attempting to write to `.git/FILENAME` fails with ENOENT because `.git` is not a directory + +#### Solution +Write temp files directly in the worktree root directory instead of trying to write inside `.git`: + +```typescript +// Fixed version +const tempFile = join(worktreePath, 'COMMIT_MSG_TEMP'); +``` + +This is safe because: +1. The temp file is immediately deleted after use +2. It won't conflict with project files (unique name) +3. Works identically in both regular repos and worktrees + +### Status: Ready to implement fix \ No newline at end of file diff --git a/main/src/services/commitManager.ts b/main/src/services/commitManager.ts index eadec6d3..694586c9 100644 --- a/main/src/services/commitManager.ts +++ b/main/src/services/commitManager.ts @@ -2,6 +2,8 @@ import { EventEmitter } from 'events'; import type { Logger } from '../utils/logger'; import { execSync } from '../utils/commandExecutor'; import { buildGitCommitCommand } from '../utils/shellEscape'; +import { writeFileSync, unlinkSync } from 'fs'; +import { join } from 'path'; import { CommitModeSettings, CommitResult, @@ -93,10 +95,20 @@ export class CommitManager extends EventEmitter { const fullMessage = prefix + commitMessage; // For checkpoint mode, use a simple commit without the extra signature - // Escape the message properly for the shell - const escapedMessage = fullMessage.replace(/'/g, "'\\''"); - const commitCommand = `git commit -m '${escapedMessage}' --no-verify`; + // Write commit message to a temporary file to avoid shell escaping issues + // Note: Write to worktree root, not .git folder (which is a file in worktrees, not a directory) + const tempFile = join(worktreePath, 'COMMIT_MSG_TEMP'); + writeFileSync(tempFile, fullMessage, 'utf8'); + + const commitCommand = `git commit -F "${tempFile}" --no-verify`; const result = execSync(commitCommand, { cwd: worktreePath, encoding: 'utf8' }); + + // Clean up temp file + try { + unlinkSync(tempFile); + } catch { + // Ignore cleanup errors + } // Extract commit hash from output const commitHashMatch = result.match(/\[[\w-]+ ([a-f0-9]+)\]/); diff --git a/main/src/services/worktreeManager.ts b/main/src/services/worktreeManager.ts index 6452bc6a..488b74b0 100644 --- a/main/src/services/worktreeManager.ts +++ b/main/src/services/worktreeManager.ts @@ -485,14 +485,24 @@ export class WorktreeManager { lastOutput = resetResult.stdout || resetResult.stderr || ''; console.log(`[WorktreeManager] Reset to base commit`); - // Properly escape commit message for cross-platform compatibility - const escapedMessage = commitMessage.replace(/"/g, '\\"'); - command = `git commit -m "${escapedMessage}"`; - executedCommands.push(`git commit -m "..." (in ${worktreePath})`); + // Write commit message to a temporary file to avoid shell escaping issues + // Note: Write to worktree root, not .git folder (which is a file in worktrees, not a directory) + const tempFile = join(worktreePath, 'SQUASH_MSG'); + await fs.writeFile(tempFile, commitMessage, 'utf8'); + + command = `git commit -F "${tempFile}"`; + executedCommands.push(`git commit -F SQUASH_MSG (in ${worktreePath})`); const commitResult = await execWithShellPath(command, { cwd: worktreePath }); lastOutput = commitResult.stdout || commitResult.stderr || ''; console.log(`[WorktreeManager] Created squashed commit`); + // Clean up temp file + try { + await fs.unlink(tempFile); + } catch { + // Ignore cleanup errors + } + // Switch to main branch in the main repository command = `git checkout ${mainBranch}`; executedCommands.push(`git checkout ${mainBranch} (in ${projectPath})`); From d808dc0579f8fd15451b1ced8e71b11bcd7f4609 Mon Sep 17 00:00:00 2001 From: Sallvainian Date: Wed, 6 Aug 2025 16:50:48 -0400 Subject: [PATCH 11/14] Fix excessive logging and Windows compatibility issues - Add debug mode toggle separate from verbose logging - Create DebugContext for component-level debug control - Replace console.log with conditional debugLog function - Fix main process log forwarding to respect verbose setting - Fix Windows 11 wmic deprecated command using PowerShell fallback - Fix Windows test setup replacing Unix touch command with fs.writeFileSync - Add .grok/ to .gitignore - Significantly reduce console output for better debugging experience Fixes issues with excessive logging that made debugging difficult. Also resolves Windows-specific compatibility problems in tests and worktree management. --- .gitignore | 1 + .../components/DraggableProjectTreeView.tsx | 5 +- frontend/src/components/Settings.tsx | 41 +++-- frontend/src/contexts/DebugContext.tsx | 63 ++++++++ frontend/src/hooks/useNotifications.ts | 9 +- frontend/src/hooks/useSessionView.ts | 141 +++++++++--------- frontend/src/main.tsx | 5 +- frontend/src/stores/sessionStore.ts | 39 ++--- frontend/src/types/config.ts | 1 + main/src/index.ts | 46 +++++- main/src/ipc/config.ts | 15 +- main/src/services/claudeCodeManager.ts | 9 +- main/src/services/configManager.ts | 15 +- main/src/services/gitStatusManager.ts | 27 ++-- main/src/services/terminalSessionManager.ts | 46 +----- main/src/types/config.ts | 2 + main/src/utils/consoleWrapper.ts | 55 +++++-- main/src/utils/processUtils.ts | 137 +++++++++++++++++ main/src/utils/shellDetector.ts | 75 ++++++---- main/src/utils/shellEscape.ts | 15 +- tests/setup.ts | 11 +- 21 files changed, 535 insertions(+), 223 deletions(-) create mode 100644 frontend/src/contexts/DebugContext.tsx create mode 100644 main/src/utils/processUtils.ts diff --git a/.gitignore b/.gitignore index 80a1a6de..0317bbdc 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,4 @@ shared/*.js shared/*.js.map shared/*.d.ts shared/*.d.ts.map +.grok/ diff --git a/frontend/src/components/DraggableProjectTreeView.tsx b/frontend/src/components/DraggableProjectTreeView.tsx index 44769912..db88ff96 100644 --- a/frontend/src/components/DraggableProjectTreeView.tsx +++ b/frontend/src/components/DraggableProjectTreeView.tsx @@ -101,8 +101,9 @@ export function DraggableProjectTreeView() { const now = Date.now(); const timeSinceLastRender = now - lastRenderTimeRef.current; - // Log if we're getting too many rapid re-renders (only in development) - if (process.env.NODE_ENV === 'development' && timeSinceLastRender < 100) { + // Only log rapid re-renders if debug mode is enabled + const debugMode = localStorage.getItem('crystal-debug-mode') === 'true'; + if (debugMode && timeSinceLastRender < 100) { console.warn('[DraggableProjectTreeView] Rapid re-render detected:', { renderCount: renderCountRef.current, timeSinceLastRender, diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx index fa33a60d..94db4741 100644 --- a/frontend/src/components/Settings.tsx +++ b/frontend/src/components/Settings.tsx @@ -19,6 +19,7 @@ import { import { Input, Textarea, Checkbox } from './ui/Input'; import { Button } from './ui/Button'; import { useTheme } from '../contexts/ThemeContext'; +import { useDebug } from '../contexts/DebugContext'; import { Modal, ModalHeader, ModalBody, ModalFooter } from './ui/Modal'; import { CollapsibleCard } from './ui/CollapsibleCard'; import { SettingsSection } from './ui/SettingsSection'; @@ -31,6 +32,7 @@ interface SettingsProps { export function Settings({ isOpen, onClose }: SettingsProps) { const [_config, setConfig] = useState(null); const [verbose, setVerbose] = useState(false); + const [debugMode, setDebugMode] = useState(false); const [anthropicApiKey, setAnthropicApiKey] = useState(''); const [globalSystemPrompt, setGlobalSystemPrompt] = useState(''); const [claudeExecutablePath, setClaudeExecutablePath] = useState(''); @@ -48,6 +50,7 @@ export function Settings({ isOpen, onClose }: SettingsProps) { const [activeTab, setActiveTab] = useState<'general' | 'notifications' | 'stravu'>('general'); const { updateSettings, reloadSettings } = useNotifications(); const { theme, toggleTheme } = useTheme(); + const { setDebugMode: setGlobalDebugMode } = useDebug(); useEffect(() => { if (isOpen) { @@ -62,6 +65,7 @@ export function Settings({ isOpen, onClose }: SettingsProps) { const data = response.data; setConfig(data); setVerbose(data.verbose || false); + setDebugMode(data.debugMode || false); setAnthropicApiKey(data.anthropicApiKey || ''); setGlobalSystemPrompt(data.systemPromptAppend || ''); setClaudeExecutablePath(data.claudeExecutablePath || ''); @@ -86,7 +90,8 @@ export function Settings({ isOpen, onClose }: SettingsProps) { try { const response = await API.config.update({ - verbose, + verbose, + debugMode, anthropicApiKey, systemPromptAppend: globalSystemPrompt, claudeExecutablePath, @@ -347,14 +352,32 @@ export function Settings({ isOpen, onClose }: SettingsProps) { description="Enable detailed logging for troubleshooting" icon={} > - setVerbose(e.target.checked)} - /> -

- Shows detailed logs for session creation and Claude Code execution. Useful for debugging issues. -

+
+
+ setVerbose(e.target.checked)} + /> +

+ Shows main process logs in the developer console. Helps troubleshoot session and Claude Code issues. +

+
+ +
+ { + setDebugMode(e.target.checked); + setGlobalDebugMode(e.target.checked); + }} + /> +

+ Shows detailed component debug logs, re-render warnings, and performance metrics. For advanced debugging only. +

+
+
void; +} + +const DebugContext = createContext(undefined); + +export function DebugProvider({ children }: { children: React.ReactNode }) { + const [debugMode, setDebugModeState] = useState(false); + + useEffect(() => { + // Load initial debug mode from config + loadDebugMode(); + }, []); + + const loadDebugMode = async () => { + try { + const response = await API.config.get(); + if (response.success && response.data) { + setDebugModeState(response.data.debugMode || false); + } + } catch (error) { + console.error('Failed to load debug mode:', error); + } + }; + + const setDebugMode = (enabled: boolean) => { + setDebugModeState(enabled); + // Update localStorage for immediate effect + localStorage.setItem('crystal-debug-mode', enabled ? 'true' : 'false'); + }; + + return ( + + {children} + + ); +} + +export function useDebug() { + const context = useContext(DebugContext); + if (context === undefined) { + // Fallback to localStorage if context is not available + const debugMode = localStorage.getItem('crystal-debug-mode') === 'true'; + return { debugMode, setDebugMode: () => {} }; + } + return context; +} + +// Helper function for conditional debug logging +export function debugLog(component: string, message: string, data?: any) { + const debugMode = localStorage.getItem('crystal-debug-mode') === 'true'; + if (debugMode) { + if (data) { + console.log(`[${component}]`, message, data); + } else { + console.log(`[${component}]`, message); + } + } +} \ No newline at end of file diff --git a/frontend/src/hooks/useNotifications.ts b/frontend/src/hooks/useNotifications.ts index 6592939d..48c2df9f 100644 --- a/frontend/src/hooks/useNotifications.ts +++ b/frontend/src/hooks/useNotifications.ts @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import { useSessionStore } from '../stores/sessionStore'; import { API } from '../utils/api'; +import { debugLog } from '../contexts/DebugContext'; interface NotificationSettings { enabled: boolean; @@ -68,7 +69,7 @@ export function useNotifications() { const showNotification = (title: string, body: string, icon?: string) => { if (!settings.enabled) { - console.log('[useNotifications] Notifications disabled, skipping:', title); + debugLog('useNotifications', '[useNotifications] Notifications disabled, skipping:', title); return; } @@ -117,7 +118,7 @@ export function useNotifications() { // If this is the initial load (prevSessions is empty and we have sessions), // just update the ref without triggering notifications if (!initialLoadComplete.current && prevSessions.length === 0 && sessions.length > 0) { - console.log('[useNotifications] Initial session load detected, skipping notifications for', sessions.length, 'sessions'); + debugLog('useNotifications', `[useNotifications] Initial session load detected, skipping notifications for ${sessions.length} sessions`); prevSessionsRef.current = sessions; initialLoadComplete.current = true; return; @@ -129,7 +130,7 @@ export function useNotifications() { } // Debug log current settings state - console.log('[useNotifications] Current settings:', settings); + debugLog('useNotifications', 'Current settings:', settings); // Compare current sessions with previous sessions to detect changes sessions.forEach((currentSession) => { @@ -201,7 +202,7 @@ export function useNotifications() { // Listen for settings update events const handleSettingsUpdate = () => { - console.log('[useNotifications] Settings updated, reloading...'); + debugLog('useNotifications', '[useNotifications] Settings updated, reloading...'); loadSettings(); }; diff --git a/frontend/src/hooks/useSessionView.ts b/frontend/src/hooks/useSessionView.ts index 59ea7946..10dc2b6d 100644 --- a/frontend/src/hooks/useSessionView.ts +++ b/frontend/src/hooks/useSessionView.ts @@ -8,6 +8,7 @@ import { FitAddon } from '@xterm/addon-fit'; import { Session, GitCommands, GitErrorDetails } from '../types/session'; import { getTerminalTheme, getScriptTerminalTheme } from '../utils/terminalTheme'; import { createVisibilityAwareInterval } from '../utils/performanceUtils'; +import { debugLog } from '../contexts/DebugContext'; export type ViewMode = 'richOutput' | 'changes' | 'terminal' | 'editor'; @@ -82,7 +83,7 @@ export const useSessionView = ( // Debug function to check state health const debugState = useCallback(() => { - console.log('[DEBUG STATE]', { + debugLog('useSessionView', '[DEBUG STATE]', { loadingRef: loadingRef.current, outputLoadState, activeSessionId, @@ -98,7 +99,7 @@ export const useSessionView = ( // Force reset stuck state const forceResetLoadingState = useCallback(() => { - console.log('[forceResetLoadingState] Forcing reset of all loading states'); + debugLog('useSessionView', '[forceResetLoadingState] Forcing reset of all loading states'); loadingRef.current = false; loadingSessionIdRef.current = null; setIsLoadingOutput(false); @@ -115,7 +116,7 @@ export const useSessionView = ( const loadOutputContent = useCallback(async (sessionId: string, retryCount = 0) => { - console.log(`[loadOutputContent] Called for session ${sessionId}, retry: ${retryCount}, loadingRef: ${loadingRef.current}`); + debugLog('useSessionView', `[loadOutputContent] Called for session ${sessionId}, retry: ${retryCount}, loadingRef: ${loadingRef.current}`); // Cancel any existing load request if (abortControllerRef.current) { @@ -131,13 +132,13 @@ export const useSessionView = ( // Check if already loading this session if (loadingRef.current && loadingSessionIdRef.current === sessionId) { - console.log(`[loadOutputContent] Already loading session ${sessionId}, skipping`); + debugLog('useSessionView', `[loadOutputContent] Already loading session ${sessionId}, skipping`); return; } // If loading a different session, abort the old one if (loadingRef.current && loadingSessionIdRef.current !== sessionId) { - console.log(`[loadOutputContent] Currently loading session ${loadingSessionIdRef.current}, will switch to ${sessionId}`); + debugLog('useSessionView', `[loadOutputContent] Currently loading session ${loadingSessionIdRef.current}, will switch to ${sessionId}`); loadingRef.current = false; loadingSessionIdRef.current = null; } @@ -145,7 +146,7 @@ export const useSessionView = ( // Check if session is still active const currentActiveSession = useSessionStore.getState().getActiveSession(); if (!currentActiveSession || currentActiveSession.id !== sessionId) { - console.log(`[loadOutputContent] Session ${sessionId} not active, skipping`); + debugLog('useSessionView', `[loadOutputContent] Session ${sessionId} not active, skipping`); return; } @@ -169,7 +170,7 @@ export const useSessionView = ( if (!response.success) { // Check if the session was archived (404 error) if (response.error && response.error.includes('not found')) { - console.log(`[loadOutputContent] Session ${sessionId} not found (possibly archived), aborting`); + debugLog('useSessionView', `[loadOutputContent] Session ${sessionId} not found (possibly archived), aborting`); // CRITICAL: Reset loading state before returning loadingRef.current = false; loadingSessionIdRef.current = null; @@ -186,12 +187,12 @@ export const useSessionView = ( } const outputs = response.data || []; - console.log(`[loadOutputContent] Received ${outputs.length} outputs for session ${sessionId}`); + debugLog('useSessionView', `[loadOutputContent] Received ${outputs.length} outputs for session ${sessionId}`); // Check if still the active session after async operation const stillActiveSession = useSessionStore.getState().getActiveSession(); if (!stillActiveSession || stillActiveSession.id !== sessionId) { - console.log(`[loadOutputContent] Session ${sessionId} no longer active, aborting`); + debugLog('useSessionView', `[loadOutputContent] Session ${sessionId} no longer active, aborting`); // CRITICAL: Reset loading state before returning loadingRef.current = false; loadingSessionIdRef.current = null; @@ -206,7 +207,7 @@ export const useSessionView = ( } // Set outputs - console.log(`[loadOutputContent] Setting outputs in store for session ${sessionId}, count: ${outputs.length}`); + debugLog('useSessionView', `[loadOutputContent] Setting outputs in store for session ${sessionId}, count: ${outputs.length}`); useSessionStore.getState().setSessionOutputs(sessionId, outputs); // Outputs have been set @@ -219,14 +220,14 @@ export const useSessionView = ( // Reset continuing conversation flag after successfully loading output if (isContinuingConversationRef.current) { - console.log(`[loadOutputContent] Resetting continuing conversation flag after output load`); + debugLog('useSessionView', `[loadOutputContent] Resetting continuing conversation flag after output load`); isContinuingConversationRef.current = false; } setLoadError(null); } catch (error: any) { if (error.name === 'AbortError') { - console.log(`[loadOutputContent] Request aborted for session ${sessionId}`); + debugLog('useSessionView', `[loadOutputContent] Request aborted for session ${sessionId}`); // CRITICAL: Reset loading state before returning loadingRef.current = false; loadingSessionIdRef.current = null; @@ -244,7 +245,7 @@ export const useSessionView = ( if (retryCount < maxRetries) { const delay = 1000 * (retryCount + 1); - console.log(`[loadOutputContent] Retrying in ${delay}ms for session ${sessionId}`); + debugLog('useSessionView', `[loadOutputContent] Retrying in ${delay}ms for session ${sessionId}`); // Reset loading state before retry loadingRef.current = false; loadingSessionIdRef.current = null; @@ -318,7 +319,7 @@ export const useSessionView = ( const currentSessionId = activeSession?.id || null; if (currentSessionId === previousSessionIdRef.current) return; - console.log(`[useSessionView] Session changed from ${previousSessionIdRef.current} to ${currentSessionId}`); + debugLog('useSessionView', `[useSessionView] Session changed from ${previousSessionIdRef.current} to ${currentSessionId}`); previousSessionIdRef.current = currentSessionId; // Force reset any stuck loading state when switching sessions @@ -341,14 +342,14 @@ export const useSessionView = ( // Clear terminal immediately when session changes if (terminalInstance.current) { - console.log(`[useSessionView] Clearing terminal for session switch`); + debugLog('useSessionView', `[useSessionView] Clearing terminal for session switch`); terminalInstance.current.clear(); } setFormattedOutput(''); lastProcessedOutputLength.current = 0; if (!activeSession) { - console.log(`[useSessionView] No active session, returning`); + debugLog('useSessionView', `[useSessionView] No active session, returning`); setCurrentSessionIdForOutput(null); // Clear any error states when no session is active setLoadError(null); @@ -356,7 +357,7 @@ export const useSessionView = ( return; } - console.log(`[useSessionView] Setting up for session ${activeSession.id}, status: ${activeSession.status}`); + debugLog('useSessionView', `[useSessionView] Setting up for session ${activeSession.id}, status: ${activeSession.status}`); setCurrentSessionIdForOutput(activeSession.id); // Check if session has conversation history @@ -403,7 +404,7 @@ export const useSessionView = ( // Make sure we're tracking the right session for output if (currentSessionIdForOutput !== activeSession.id) { - console.log(`[useSessionView] Session ID mismatch in format effect - current: ${currentSessionIdForOutput}, active: ${activeSession.id}`); + debugLog('useSessionView', `[useSessionView] Session ID mismatch in format effect - current: ${currentSessionIdForOutput}, active: ${activeSession.id}`); // If the session ID doesn't match, update it if (activeSession.id) { setCurrentSessionIdForOutput(activeSession.id); @@ -422,16 +423,16 @@ export const useSessionView = ( const formatOutput = () => { const currentActiveSession = useSessionStore.getState().getActiveSession(); if (!currentActiveSession || currentActiveSession.id !== activeSession.id) { - console.log(`[formatOutput] Session changed during formatting, aborting`); + debugLog('useSessionView', `[formatOutput] Session changed during formatting, aborting`); return; } // The output array should already contain formatted strings from the IPC handler const outputArray = currentActiveSession.output || []; - console.log(`[formatOutput] Session ${activeSession.id} has ${outputArray.length} output items`); + debugLog('useSessionView', `[formatOutput] Session ${activeSession.id} has ${outputArray.length} output items`); if (outputArray.length > 0) { - console.log(`[formatOutput] First output item preview: ${outputArray[0].substring(0, 200)}`); + debugLog('useSessionView', `[formatOutput] First output item preview: ${outputArray[0].substring(0, 200)}`); } const formatted = outputArray.join(''); @@ -439,10 +440,10 @@ export const useSessionView = ( // Double-check we're still on the same session before updating const finalActiveSession = useSessionStore.getState().getActiveSession(); if (finalActiveSession && finalActiveSession.id === activeSession.id && currentSessionIdForOutput === activeSession.id) { - console.log(`[formatOutput] Setting formatted output for session ${activeSession.id}, length: ${formatted.length}, first 100 chars: ${formatted.substring(0, 100)}`); + debugLog('useSessionView', `[formatOutput] Setting formatted output for session ${activeSession.id}, length: ${formatted.length}, first 100 chars: ${formatted.substring(0, 100)}`); setFormattedOutput(formatted); } else { - console.log(`[formatOutput] Session mismatch during final check - finalActiveSession.id: ${finalActiveSession?.id}, activeSession.id: ${activeSession.id}, currentSessionIdForOutput: ${currentSessionIdForOutput}`); + debugLog('useSessionView', `[formatOutput] Session mismatch during final check - finalActiveSession.id: ${finalActiveSession?.id}, activeSession.id: ${activeSession.id}, currentSessionIdForOutput: ${currentSessionIdForOutput}`); } }; @@ -454,7 +455,7 @@ export const useSessionView = ( // Consolidated effect for loading output useEffect(() => { - console.log(`[Output Load Effect] Checking - activeSession: ${activeSession?.id}, currentSessionIdForOutput: ${currentSessionIdForOutput}, outputLoadState: ${outputLoadState}, loadingRef: ${loadingRef.current}, loadingSessionId: ${loadingSessionIdRef.current}`); + debugLog('useSessionView', `[Output Load Effect] Checking - activeSession: ${activeSession?.id}, currentSessionIdForOutput: ${currentSessionIdForOutput}, outputLoadState: ${outputLoadState}, loadingRef: ${loadingRef.current}, loadingSessionId: ${loadingSessionIdRef.current}`); if (!activeSession || !currentSessionIdForOutput || currentSessionIdForOutput !== activeSession.id) { return; @@ -462,7 +463,7 @@ export const useSessionView = ( // Skip initial load if continuing conversation, but allow explicit reloads if (isContinuingConversationRef.current && outputLoadState === 'idle' && !shouldReloadOutput) { - console.log(`[Output Load Effect] Skipping initial load - continuing conversation`); + debugLog('useSessionView', `[Output Load Effect] Skipping initial load - continuing conversation`); return; } @@ -490,13 +491,13 @@ export const useSessionView = ( setShouldReloadOutput(false); } else if (outputLoadState === 'error' && !loadingRef.current) { // Retry after error if not currently loading - console.log(`[Output Load Effect] Previous load errored, retrying`); + debugLog('useSessionView', `[Output Load Effect] Previous load errored, retrying`); shouldLoad = true; loadDelay = 1000; } if (shouldLoad && !loadingRef.current) { - console.log(`[Output Load Effect] Scheduling load for session ${activeSession.id} in ${loadDelay}ms`); + debugLog('useSessionView', `[Output Load Effect] Scheduling load for session ${activeSession.id} in ${loadDelay}ms`); if (loadDelay > 0) { outputLoadTimeoutRef.current = setTimeout(() => { if (!loadingRef.current) { @@ -507,7 +508,7 @@ export const useSessionView = ( loadOutputContent(activeSession.id); } } else if (shouldLoad && loadingRef.current) { - console.log(`[Output Load Effect] Want to load but already loading, will retry later`); + debugLog('useSessionView', `[Output Load Effect] Want to load but already loading, will retry later`); } }, [ activeSession?.id, @@ -530,7 +531,7 @@ export const useSessionView = ( if (activeSession?.id === sessionId) { // Trigger reload if we're loaded or if we're continuing a conversation if (outputLoadState === 'loaded' || isContinuingConversationRef.current) { - console.log(`[Output Available] New output for active session ${sessionId}, requesting reload (state: ${outputLoadState}, continuing: ${isContinuingConversationRef.current})`); + debugLog('useSessionView', `[Output Available] New output for active session ${sessionId}, requesting reload (state: ${outputLoadState}, continuing: ${isContinuingConversationRef.current})`); setShouldReloadOutput(true); } } @@ -541,15 +542,15 @@ export const useSessionView = ( }, [activeSession?.id, outputLoadState]); const initTerminal = useCallback((termRef: React.RefObject | undefined, instanceRef: React.MutableRefObject, fitAddonRef: React.MutableRefObject, isScript: boolean) => { - console.log(`[initTerminal] Called - termRef.current: ${!!termRef?.current}, instanceRef.current: ${!!instanceRef.current}, isScript: ${isScript}`); + debugLog('useSessionView', `[initTerminal] Called - termRef.current: ${!!termRef?.current}, instanceRef.current: ${!!instanceRef.current}, isScript: ${isScript}`); if (!termRef?.current) { - console.log(`[initTerminal] No terminal ref element, cannot initialize`); + debugLog('useSessionView', `[initTerminal] No terminal ref element, cannot initialize`); return; } if (instanceRef.current) { - console.log(`[initTerminal] Terminal instance already exists, skipping`); + debugLog('useSessionView', `[initTerminal] Terminal instance already exists, skipping`); return; } @@ -577,7 +578,7 @@ export const useSessionView = ( instanceRef.current = term; fitAddonRef.current = addon; - console.log(`[initTerminal] Terminal initialized successfully`); + debugLog('useSessionView', `[initTerminal] Terminal initialized successfully`); if (isScript) { // Clear any existing content @@ -609,13 +610,13 @@ export const useSessionView = ( // Pre-initialize script terminal when session becomes active useEffect(() => { if (activeSession && scriptTerminalRef.current && !scriptTerminalInstance.current) { - console.log('[Terminal] Pre-initializing script terminal for session', activeSession.id); + debugLog('useSessionView', '[Terminal] Pre-initializing script terminal for session', activeSession.id); initTerminal(scriptTerminalRef, scriptTerminalInstance, scriptFitAddon, true); // Also pre-create the backend PTY session API.sessions.preCreateTerminal(activeSession.id).then(response => { if (response.success) { - console.log('[Terminal] Backend PTY pre-created for session', activeSession.id); + debugLog('useSessionView', '[Terminal] Backend PTY pre-created for session', activeSession.id); } }).catch(error => { console.error('[Terminal] Failed to pre-create backend PTY:', error); @@ -637,7 +638,7 @@ export const useSessionView = ( const currentScriptOutput = useSessionStore.getState().scriptOutput[activeSession.id] || []; if (scriptTerminalInstance.current && currentScriptOutput.length > 0 && lastProcessedScriptOutputLength.current === 0) { const existingOutput = currentScriptOutput.join(''); - console.log('[Terminal] Writing existing output to newly initialized terminal', existingOutput.length, 'chars'); + debugLog('useSessionView', `[Terminal] Writing existing output to newly initialized terminal ${existingOutput.length} chars`); scriptTerminalInstance.current.write(existingOutput); lastProcessedScriptOutputLength.current = existingOutput.length; } @@ -645,17 +646,17 @@ export const useSessionView = ( // Only send empty input if this is a fresh terminal initialization // Don't send it when just switching back to terminal view if (!wasAlreadyInitialized && !activeSession.archived) { - console.log('[Terminal] Sending empty input to trigger PTY prompt (fresh initialization)'); + debugLog('useSessionView', '[Terminal] Sending empty input to trigger PTY prompt (fresh initialization)'); API.sessions.sendTerminalInput(activeSession.id, '').catch(error => { console.error('Failed to send terminal trigger input:', error); }); } else { - console.log('[Terminal] Terminal already initialized, skipping prompt trigger'); + debugLog('useSessionView', '[Terminal] Terminal already initialized, skipping prompt trigger'); } // Focus the terminal after everything is set up if (scriptTerminalInstance.current) { - console.log('[Terminal] Focusing terminal'); + debugLog('useSessionView', '[Terminal] Focusing terminal'); scriptTerminalInstance.current.focus(); } }, 100); @@ -715,22 +716,22 @@ export const useSessionView = ( /* useEffect(() => { // Output view removed - skip terminal writing entirely if (!activeSession || !terminalInstance.current) return; - console.log(`[Terminal Write Effect] Called, formatted output length: ${formattedOutput.length}, session: ${currentSessionIdForOutput}, lastProcessed: ${lastProcessedOutputLength.current}, viewMode: ${viewMode}`); + debugLog('useSessionView', `[Terminal Write Effect] Called, formatted output length: ${formattedOutput.length}, session: ${currentSessionIdForOutput}, lastProcessed: ${lastProcessedOutputLength.current}, viewMode: ${viewMode}`); // Skip if not in output view mode // Output view removed - skip terminal writing return; if (!terminalInstance.current) { - console.log(`[Terminal Write Effect] No terminal instance yet`); + debugLog('useSessionView', `[Terminal Write Effect] No terminal instance yet`); // If we have formatted output but no terminal, retry after a delay if (formattedOutput && formattedOutput.length > 0 && terminalRef?.current) { - console.log(`[Terminal Write Effect] Have output but no terminal, attempting init`); + debugLog('useSessionView', `[Terminal Write Effect] Have output but no terminal, attempting init`); initTerminal(terminalRef, terminalInstance, fitAddon, false); // Give terminal time to initialize then write setTimeout(() => { if (terminalInstance.current && formattedOutput.length > 0 && lastProcessedOutputLength.current === 0) { - console.log(`[Terminal Write Effect] Writing buffered output after init`); + debugLog('useSessionView', `[Terminal Write Effect] Writing buffered output after init`); terminalInstance.current.write(formattedOutput); lastProcessedOutputLength.current = formattedOutput.length; // Only auto-scroll if user is already at the bottom @@ -747,27 +748,27 @@ export const useSessionView = ( } if (!formattedOutput && formattedOutput !== '') { - console.log(`[Terminal Write Effect] No formatted output`); + debugLog('useSessionView', `[Terminal Write Effect] No formatted output`); return; } const currentActiveSession = useSessionStore.getState().getActiveSession(); if (!currentActiveSession || currentSessionIdForOutput !== currentActiveSession.id) { - console.log(`[Terminal Write Effect] Session mismatch: ${currentSessionIdForOutput} !== ${currentActiveSession?.id}`); + debugLog('useSessionView', `[Terminal Write Effect] Session mismatch: ${currentSessionIdForOutput} !== ${currentActiveSession?.id}`); return; } // Write to terminal if (lastProcessedOutputLength.current === 0) { // Clear terminal and write all content for new session - console.log(`[Terminal Write Effect] New session output detected, clearing terminal and writing all content, length: ${formattedOutput.length}`); + debugLog('useSessionView', `[Terminal Write Effect] New session output detected, clearing terminal and writing all content, length: ${formattedOutput.length}`); terminalInstance.current.clear(); terminalInstance.current.write(formattedOutput); lastProcessedOutputLength.current = formattedOutput.length; } else if (formattedOutput.length > lastProcessedOutputLength.current) { // Write only new content for existing session const newContent = formattedOutput.substring(lastProcessedOutputLength.current); - console.log(`[Terminal Write Effect] Writing new content to terminal, length: ${newContent.length}`); + debugLog('useSessionView', `[Terminal Write Effect] Writing new content to terminal, length: ${newContent.length}`); terminalInstance.current.write(newContent); lastProcessedOutputLength.current = formattedOutput.length; } else if (formattedOutput.length < lastProcessedOutputLength.current) { @@ -798,7 +799,7 @@ export const useSessionView = ( } else if (fullScriptOutput.length < lastProcessedScriptOutputLength.current) { // Output got shorter but not cleared - this might be a sync issue // Don't reset, just update the tracking - console.log('[Terminal] Script output got shorter, updating tracking without reset'); + debugLog('useSessionView', '[Terminal] Script output got shorter, updating tracking without reset'); lastProcessedScriptOutputLength.current = fullScriptOutput.length; } else if (fullScriptOutput.length > lastProcessedScriptOutputLength.current) { const newOutput = fullScriptOutput.substring(lastProcessedScriptOutputLength.current); @@ -819,7 +820,7 @@ export const useSessionView = ( const handleSessionDeleted = (event: CustomEvent) => { // The event detail contains just { id } from the backend if (event.detail?.id === activeSessionId) { - console.log(`[useSessionView] Active session ${activeSessionId} was deleted/archived`); + debugLog('useSessionView', `[useSessionView] Active session ${activeSessionId} was deleted/archived`); // Force reset loading states forceResetLoadingState(); // Clear terminal @@ -862,7 +863,7 @@ export const useSessionView = ( const handleSwitchToViewDiff = (event: CustomEvent) => { const { sessionId } = event.detail; if (sessionId && activeSession?.id === sessionId) { - console.log('[useSessionView] Switching to View Diff tab for session:', sessionId); + debugLog('useSessionView', '[useSessionView] Switching to View Diff tab for session:', sessionId); setViewMode('changes'); } }; @@ -877,7 +878,7 @@ export const useSessionView = ( useEffect(() => { const handleSelectAndViewDiff = async (event: CustomEvent) => { const { sessionId } = event.detail; - console.log('[useSessionView] Select session and view diff:', sessionId); + debugLog('useSessionView', '[useSessionView] Select session and view diff:', sessionId); // First, select the session if it's not already active if (sessionId && activeSession?.id !== sessionId) { @@ -919,19 +920,19 @@ export const useSessionView = ( useEffect(() => { // Add a small delay to ensure CSS has propagated const timer = setTimeout(() => { - console.log('[Terminal Theme Update] Theme changed to:', theme); - console.log('[Terminal Theme Update] Root classes:', document.documentElement.className); + debugLog('useSessionView', '[Terminal Theme Update] Theme changed to:', theme); + debugLog('useSessionView', '[Terminal Theme Update] Root classes:', document.documentElement.className); if (terminalInstance.current) { const newTheme = getTerminalTheme(); - console.log('[Terminal Theme Update] New terminal theme:', newTheme); + debugLog('useSessionView', '[Terminal Theme Update] New terminal theme:', newTheme); terminalInstance.current.options.theme = newTheme; // Force refresh to apply new colors terminalInstance.current.refresh(0, terminalInstance.current.rows - 1); } if (scriptTerminalInstance.current) { const newScriptTheme = getScriptTerminalTheme(); - console.log('[Terminal Theme Update] New script terminal theme:', newScriptTheme); + debugLog('useSessionView', '[Terminal Theme Update] New script terminal theme:', newScriptTheme); scriptTerminalInstance.current.options.theme = newScriptTheme; // Force refresh to apply new colors scriptTerminalInstance.current.refresh(0, scriptTerminalInstance.current.rows - 1); @@ -1111,14 +1112,14 @@ export const useSessionView = ( const handleDebugKeyboard = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'D') { e.preventDefault(); - console.log('=== DEBUG STATE DUMP ==='); + debugLog('useSessionView', '=== DEBUG STATE DUMP ==='); debugState(); - console.log('=== END DEBUG STATE ==='); + debugLog('useSessionView', '=== END DEBUG STATE ==='); } // Force reset with Cmd/Ctrl + Shift + R if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'R') { e.preventDefault(); - console.log('=== FORCE RESET LOADING STATE ==='); + debugLog('useSessionView', '=== FORCE RESET LOADING STATE ==='); forceResetLoadingState(); setShouldReloadOutput(true); } @@ -1128,9 +1129,9 @@ export const useSessionView = ( }, [debugState, forceResetLoadingState]); const handleSendInput = async (attachedImages?: any[]) => { - console.log('[useSessionView] handleSendInput called', { input, activeSession: activeSession?.id, hasActiveSession: !!activeSession }); + debugLog('useSessionView', '[useSessionView] handleSendInput called', { input, activeSession: activeSession?.id, hasActiveSession: !!activeSession }); if (!input.trim() || !activeSession) { - console.log('[useSessionView] handleSendInput early return', { inputTrimmed: !input.trim(), noActiveSession: !activeSession }); + debugLog('useSessionView', '[useSessionView] handleSendInput early return', { inputTrimmed: !input.trim(), noActiveSession: !activeSession }); return; } @@ -1138,7 +1139,7 @@ export const useSessionView = ( // Check if we have compacted context to inject if (contextCompacted && compactedContext) { - console.log('[Context Compaction] Injecting compacted context into prompt'); + debugLog('useSessionView', '[Context Compaction] Injecting compacted context into prompt'); finalInput = `\n${compactedContext}\n\n\n${finalInput}`; // Clear the compacted context after using it @@ -1185,7 +1186,7 @@ export const useSessionView = ( // Check if we have compacted context to inject if (contextCompacted && compactedContext) { - console.log('[Context Compaction] Injecting compacted context into continuation prompt'); + debugLog('useSessionView', '[Context Compaction] Injecting compacted context into continuation prompt'); finalInput = `\n${compactedContext}\n\n\n${finalInput}`; // Clear the compacted context after using it @@ -1292,13 +1293,13 @@ export const useSessionView = ( const handleRebaseMainIntoWorktree = async () => { if (!activeSession) return; - console.log(`[handleRebaseMainIntoWorktree] Starting rebase for session ${activeSession.id}`); + debugLog('useSessionView', `[handleRebaseMainIntoWorktree] Starting rebase for session ${activeSession.id}`); setIsMerging(true); setMergeError(null); try { - console.log(`[handleRebaseMainIntoWorktree] Calling API.sessions.rebaseMainIntoWorktree`); + debugLog('useSessionView', `[handleRebaseMainIntoWorktree] Calling API.sessions.rebaseMainIntoWorktree`); const response = await API.sessions.rebaseMainIntoWorktree(activeSession.id); - console.log(`[handleRebaseMainIntoWorktree] API call completed`, response); + debugLog('useSessionView', `[handleRebaseMainIntoWorktree] API call completed`, response); if (!response.success) { if ((response as any).gitError) { @@ -1316,10 +1317,10 @@ export const useSessionView = ( setMergeError(response.error || 'Failed to rebase main into worktree'); } } else { - console.log(`[handleRebaseMainIntoWorktree] Rebase successful, checking for changes to rebase`); + debugLog('useSessionView', `[handleRebaseMainIntoWorktree] Rebase successful, checking for changes to rebase`); // Run this in the background and don't let it block the finally block API.sessions.hasChangesToRebase(activeSession.id).then(changesResponse => { - console.log(`[handleRebaseMainIntoWorktree] hasChangesToRebase completed`, changesResponse); + debugLog('useSessionView', `[handleRebaseMainIntoWorktree] hasChangesToRebase completed`, changesResponse); if (changesResponse.success) setHasChangesToRebase(changesResponse.data); }).catch(error => { console.error(`[handleRebaseMainIntoWorktree] hasChangesToRebase failed`, error); @@ -1329,7 +1330,7 @@ export const useSessionView = ( console.error(`[handleRebaseMainIntoWorktree] Error in try block`, error); setMergeError(error instanceof Error ? error.message : 'Failed to rebase main into worktree'); } finally { - console.log(`[handleRebaseMainIntoWorktree] Finally block executing, setting isMerging to false`); + debugLog('useSessionView', `[handleRebaseMainIntoWorktree] Finally block executing, setting isMerging to false`); setIsMerging(false); } }; @@ -1540,7 +1541,7 @@ export const useSessionView = ( if (!activeSession) return; try { - console.log('[Context Compaction] Starting compaction for session:', activeSession.id); + debugLog('useSessionView', '[Context Compaction] Starting compaction for session:', activeSession.id); // Generate the compacted context const response = await API.sessions.generateCompactedContext(activeSession.id); @@ -1574,7 +1575,7 @@ export const useSessionView = ( terminalInstance.current.scrollToBottom(); } - console.log('[Context Compaction] Context successfully compacted and displayed'); + debugLog('useSessionView', '[Context Compaction] Context successfully compacted and displayed'); } else { console.error('[Context Compaction] Failed to compact context:', response.error); if (terminalInstance.current) { diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 690c99bc..741fa560 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,13 +2,16 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import { ThemeProvider } from './contexts/ThemeContext'; +import { DebugProvider } from './contexts/DebugContext'; import './index.css'; import './styles/markdown-preview.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - + + + , ); \ No newline at end of file diff --git a/frontend/src/stores/sessionStore.ts b/frontend/src/stores/sessionStore.ts index 5e8c5294..f7eb119c 100644 --- a/frontend/src/stores/sessionStore.ts +++ b/frontend/src/stores/sessionStore.ts @@ -1,6 +1,7 @@ import { create } from 'zustand'; import type { Session, SessionOutput, GitStatus } from '../types/session'; import { API } from '../utils/api'; +import { debugLog } from '../contexts/DebugContext'; interface CreateSessionRequest { prompt: string; @@ -73,7 +74,7 @@ export const useSessionStore = create((set, get) => ({ loadSessions: (sessions) => set({ sessions, isLoaded: true }), addSession: (session) => set((state) => { - console.log(`[SessionStore] Adding new session ${session.id} and setting as active`); + debugLog('SessionStore', `[SessionStore] Adding new session ${session.id} and setting as active`); // Initialize arrays if they don't exist const sessionWithArrays = { @@ -98,7 +99,7 @@ export const useSessionStore = create((set, get) => ({ output: state.activeMainRepoSession.output, jsonMessages: state.activeMainRepoSession.jsonMessages }; - console.log(`[SessionStore] Updated active main repo session ${updatedSession.id} model:`, newActiveSession.model); + debugLog('SessionStore', `[SessionStore] Updated active main repo session ${updatedSession.id} model:`, newActiveSession.model); return { ...state, activeMainRepoSession: newActiveSession @@ -114,7 +115,7 @@ export const useSessionStore = create((set, get) => ({ output: session.output, jsonMessages: session.jsonMessages }; - console.log(`[SessionStore] Updated session ${updatedSession.id} model: ${session.model} -> ${updatedSessionWithOutput.model}`); + debugLog('SessionStore', `[SessionStore] Updated session ${updatedSession.id} model: ${session.model} -> ${updatedSessionWithOutput.model}`); return updatedSessionWithOutput; } return session; @@ -140,7 +141,7 @@ export const useSessionStore = create((set, get) => ({ }), setActiveSession: async (sessionId) => { - console.log('[SessionStore] setActiveSession called with:', sessionId); + debugLog('SessionStore', '[SessionStore] setActiveSession called with:', sessionId); if (!sessionId) { set({ activeSessionId: null, activeMainRepoSession: null }); @@ -152,11 +153,11 @@ export const useSessionStore = create((set, get) => ({ const existingSession = state.sessions.find(s => s.id === sessionId); if (existingSession) { - console.log('[SessionStore] Session found in local store:', existingSession.id, existingSession.name); + debugLog('SessionStore', `[SessionStore] Session found in local store: ${existingSession.id} ${existingSession.name}`); if (existingSession.isMainRepo) { // Store main repo session separately with initialized arrays - console.log('[SessionStore] Setting existing main repo session as active'); + debugLog('SessionStore', '[SessionStore] Setting existing main repo session as active'); set({ activeSessionId: sessionId, activeMainRepoSession: { @@ -167,7 +168,7 @@ export const useSessionStore = create((set, get) => ({ }); } else { // Regular session - just set the ID - console.log('[SessionStore] Setting existing regular session as active'); + debugLog('SessionStore', '[SessionStore] Setting existing regular session as active'); set({ activeSessionId: sessionId, activeMainRepoSession: null }); } @@ -182,19 +183,19 @@ export const useSessionStore = create((set, get) => ({ // If not in local store, fetch from backend (this might be a stale UI) try { - console.log('[SessionStore] Session not in local store, fetching from backend'); + debugLog('SessionStore', '[SessionStore] Session not in local store, fetching from backend'); const response = await API.sessions.get(sessionId); - console.log('[SessionStore] Session fetch response:', response); + debugLog('SessionStore', '[SessionStore] Session fetch response:', response); if (response.success && response.data) { const session = response.data; - console.log('[SessionStore] Session data from backend:', session); + debugLog('SessionStore', '[SessionStore] Session data from backend:', session); // Add the session to local store if not already there const currentSessions = get().sessions; const sessionExists = currentSessions.find(s => s.id === sessionId); if (!sessionExists) { - console.log('[SessionStore] Adding fetched session to local store'); + debugLog('SessionStore', '[SessionStore] Adding fetched session to local store'); set(state => ({ sessions: [...state.sessions, { ...session, @@ -206,7 +207,7 @@ export const useSessionStore = create((set, get) => ({ if (session.isMainRepo) { // Store main repo session separately with initialized arrays - console.log('[SessionStore] Setting fetched main repo session as active'); + debugLog('SessionStore', '[SessionStore] Setting fetched main repo session as active'); set({ activeSessionId: sessionId, activeMainRepoSession: { @@ -217,7 +218,7 @@ export const useSessionStore = create((set, get) => ({ }); } else { // Regular session - console.log('[SessionStore] Setting fetched regular session as active'); + debugLog('SessionStore', '[SessionStore] Setting fetched regular session as active'); set({ activeSessionId: sessionId, activeMainRepoSession: null }); } // Only mark session as viewed if it wasn't already active @@ -236,7 +237,7 @@ export const useSessionStore = create((set, get) => ({ }, addSessionOutput: (output) => set((state) => { - console.log(`[SessionStore] Adding output for session ${output.sessionId}, type: ${output.type}`); + debugLog('SessionStore', `[SessionStore] Adding output for session ${output.sessionId}, type: ${output.type}`); // Find session in sessions array const sessionIndex = state.sessions.findIndex(s => s.id === output.sessionId); @@ -304,7 +305,7 @@ export const useSessionStore = create((set, get) => ({ }), setSessionOutputs: (sessionId, outputs) => set((state) => { - console.log(`[SessionStore] Setting ${outputs.length} outputs for session ${sessionId}`); + debugLog('SessionStore', `[SessionStore] Setting ${outputs.length} outputs for session ${sessionId}`); // Separate outputs and JSON messages const stdOutputs: string[] = []; @@ -330,7 +331,7 @@ export const useSessionStore = create((set, get) => ({ // Also update activeMainRepoSession if it matches let updatedActiveMainRepoSession = state.activeMainRepoSession; if (state.activeMainRepoSession && state.activeMainRepoSession.id === sessionId) { - console.log(`[SessionStore] Also updating activeMainRepoSession`); + debugLog('SessionStore', `[SessionStore] Also updating activeMainRepoSession`); updatedActiveMainRepoSession = { ...state.activeMainRepoSession, output: stdOutputs, jsonMessages }; } @@ -401,17 +402,17 @@ export const useSessionStore = create((set, get) => ({ getActiveSession: () => { const state = get(); - console.log('[SessionStore] getActiveSession - activeSessionId:', state.activeSessionId, 'sessions count:', state.sessions.length); + debugLog('SessionStore', `[SessionStore] getActiveSession - activeSessionId: ${state.activeSessionId}, sessions count: ${state.sessions.length}`); // If we have a main repo session, return it if (state.activeMainRepoSession && state.activeMainRepoSession.id === state.activeSessionId) { - console.log('[SessionStore] Returning activeMainRepoSession'); + debugLog('SessionStore', '[SessionStore] Returning activeMainRepoSession'); return state.activeMainRepoSession; } // Otherwise look in regular sessions const found = state.sessions.find(session => session.id === state.activeSessionId); - console.log('[SessionStore] Found session in sessions array:', found?.id, found?.name); + debugLog('SessionStore', `[SessionStore] Found session in sessions array: ${found?.id} ${found?.name}`); return found; }, diff --git a/frontend/src/types/config.ts b/frontend/src/types/config.ts index d4516128..03960b7d 100644 --- a/frontend/src/types/config.ts +++ b/frontend/src/types/config.ts @@ -1,6 +1,7 @@ export interface AppConfig { gitRepoPath: string; verbose?: boolean; + debugMode?: boolean; anthropicApiKey?: string; systemPromptAppend?: string; runScript?: string[]; diff --git a/main/src/index.ts b/main/src/index.ts index 3a4de676..2caff69c 100644 --- a/main/src/index.ts +++ b/main/src/index.ts @@ -92,6 +92,22 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && !app.isPackaged; // Set up console wrapper to reduce logging in production setupConsoleWrapper(); +// NUCLEAR OPTION: If verbose is off, silence everything except errors +try { + const Store = require('electron-store'); + const store = new Store({ name: 'crystal-settings' }); + const config = store.get('config', {}); + if (!config.verbose) { + // Override ALL console methods to be silent except errors + console.log = () => {}; + console.info = () => {}; + console.debug = () => {}; + // Keep warn and error for important messages + } +} catch (e) { + // If we can't read config, keep normal logging +} + // Parse command-line arguments for custom Crystal directory const args = process.argv.slice(2); for (let i = 0; i < args.length; i++) { @@ -243,13 +259,23 @@ async function createWindow() { originalLog.apply(console, args); } - // Forward to renderer + // Forward to renderer only if verbose mode is enabled or it's an important message if (mainWindow && !mainWindow.isDestroyed()) { - try { - mainWindow.webContents.send('main-log', 'log', message); - } catch (e) { - // If sending to renderer fails, use original console to avoid recursion - originalLog('[Main] Failed to send log to renderer:', e); + // Check if we should send this log to the renderer + const shouldSendToRenderer = configManager?.isVerbose() || + message.includes('Error') || + message.includes('Failed') || + message.includes('[Main] Window created') || + message.includes('[Main] Starting app') || + message.includes('[Main] Performing startup'); + + if (shouldSendToRenderer) { + try { + mainWindow.webContents.send('main-log', 'log', message); + } catch (e) { + // If sending to renderer fails, use original console to avoid recursion + originalLog('[Main] Failed to send log to renderer:', e); + } } } }; @@ -331,6 +357,7 @@ async function createWindow() { originalWarn.apply(console, args); } + // Always send warnings to renderer (they're important) if (mainWindow && !mainWindow.isDestroyed()) { try { mainWindow.webContents.send('main-log', 'warn', message); @@ -363,7 +390,8 @@ async function createWindow() { originalInfo.apply(console, args); } - if (mainWindow && !mainWindow.isDestroyed()) { + // Forward to renderer only if verbose mode is enabled + if (mainWindow && !mainWindow.isDestroyed() && configManager?.isVerbose()) { try { mainWindow.webContents.send('main-log', 'info', message); } catch (e) { @@ -395,6 +423,10 @@ async function createWindow() { async function initializeServices() { configManager = new ConfigManager(); await configManager.initialize(); + + // Update console wrapper with configManager to respect verbose setting + const { updateConfigManager } = require('./utils/consoleWrapper'); + updateConfigManager(configManager); // Initialize logger early so it can capture all logs logger = new Logger(configManager); diff --git a/main/src/ipc/config.ts b/main/src/ipc/config.ts index 9b520622..1764e69b 100644 --- a/main/src/ipc/config.ts +++ b/main/src/ipc/config.ts @@ -14,11 +14,18 @@ export function registerConfigHandlers(ipcMain: IpcMain, { configManager, claude ipcMain.handle('config:update', async (_event, updates: any) => { try { + console.log('[Config] Received update request with keys:', Object.keys(updates)); + // Check if Claude path is being updated const oldConfig = configManager.getConfig(); const claudePathChanged = updates.claudeExecutablePath !== undefined && updates.claudeExecutablePath !== oldConfig.claudeExecutablePath; + if (claudePathChanged) { + console.log('[Config] Claude path changing from:', oldConfig.claudeExecutablePath); + console.log('[Config] Claude path changing to:', updates.claudeExecutablePath); + } + await configManager.updateConfig(updates); // Clear Claude availability cache if the path changed @@ -29,8 +36,12 @@ export function registerConfigHandlers(ipcMain: IpcMain, { configManager, claude return { success: true }; } catch (error) { - console.error('Failed to update config:', error); - return { success: false, error: 'Failed to update config' }; + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + const errorStack = error instanceof Error ? error.stack : ''; + console.error('[Config] Failed to update config:', errorMessage); + console.error('[Config] Error stack:', errorStack); + console.error('[Config] Updates that failed:', updates); + return { success: false, error: `Failed to update config: ${errorMessage}` }; } }); } \ No newline at end of file diff --git a/main/src/services/claudeCodeManager.ts b/main/src/services/claudeCodeManager.ts index ffbc4c1c..c6724d0b 100644 --- a/main/src/services/claudeCodeManager.ts +++ b/main/src/services/claudeCodeManager.ts @@ -162,8 +162,7 @@ export class ClaudeCodeManager extends EventEmitter { } // Build the command arguments - // Need --verbose for Claude to output anything in plain text mode - const args: string[] = ['--verbose']; + const args: string[] = []; // Add model argument if specified if (model) { @@ -756,12 +755,6 @@ export class ClaudeCodeManager extends EventEmitter { lastOutput += data; buffer += data; - // Debug: Log raw data to see what Claude is sending - console.log(`\x1b[36m${'='.repeat(80)}\x1b[0m`); - console.log(`\x1b[36m[ClaudeManager DEBUG] Raw data from Claude (first 500 chars):\x1b[0m`); - console.log(`\x1b[33m${data.substring(0, 500).replace(/\n/g, '\\n').replace(/\r/g, '\\r')}\x1b[0m`); - console.log(`\x1b[36m${'='.repeat(80)}\x1b[0m`); - // Claude sends streaming JSON that can be split across chunks // We need to detect complete JSON objects by counting braces diff --git a/main/src/services/configManager.ts b/main/src/services/configManager.ts index 543f1792..a22e7e61 100644 --- a/main/src/services/configManager.ts +++ b/main/src/services/configManager.ts @@ -58,8 +58,19 @@ export class ConfigManager extends EventEmitter { } private async saveConfig(): Promise { - await fs.mkdir(this.configDir, { recursive: true }); - await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2)); + try { + await fs.mkdir(this.configDir, { recursive: true }); + const configString = JSON.stringify(this.config, null, 2); + console.log('[ConfigManager] Saving config to:', this.configPath); + console.log('[ConfigManager] Config size:', configString.length, 'bytes'); + await fs.writeFile(this.configPath, configString); + console.log('[ConfigManager] Config saved successfully'); + } catch (error) { + console.error('[ConfigManager] Failed to save config:', error); + console.error('[ConfigManager] Config path:', this.configPath); + console.error('[ConfigManager] Config dir:', this.configDir); + throw error; + } } getConfig(): AppConfig { diff --git a/main/src/services/gitStatusManager.ts b/main/src/services/gitStatusManager.ts index aded5650..ecda7bf7 100644 --- a/main/src/services/gitStatusManager.ts +++ b/main/src/services/gitStatusManager.ts @@ -270,22 +270,27 @@ export class GitStatusManager extends EventEmitter { // Create a promise that will be resolved after debounce this.gitLogger.logDebounce(sessionId, 'start'); - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const timer = setTimeout(async () => { this.refreshDebounceTimers.delete(sessionId); this.gitLogger.logDebounce(sessionId, 'complete'); - // Only emit loading event for user-initiated refreshes - if (isUserInitiated) { - this.emitThrottled(sessionId, 'loading'); - } - - const status = await this.fetchGitStatus(sessionId); - if (status) { - this.updateCache(sessionId, status); - this.emitThrottled(sessionId, 'updated', status); + try { + // Only emit loading event for user-initiated refreshes + if (isUserInitiated) { + this.emitThrottled(sessionId, 'loading'); + } + + const status = await this.fetchGitStatus(sessionId); + if (status) { + this.updateCache(sessionId, status); + this.emitThrottled(sessionId, 'updated', status); + } + resolve(status); + } catch (error) { + this.logger?.error(`[GitStatus] Error refreshing git status for session ${sessionId}:`, error as Error); + reject(error); } - resolve(status); }, this.DEBOUNCE_MS); this.refreshDebounceTimers.set(sessionId, timer); diff --git a/main/src/services/terminalSessionManager.ts b/main/src/services/terminalSessionManager.ts index a180ac25..1d3fd4ce 100644 --- a/main/src/services/terminalSessionManager.ts +++ b/main/src/services/terminalSessionManager.ts @@ -5,6 +5,7 @@ import { ShellDetector } from '../utils/shellDetector'; import * as os from 'os'; import { exec } from 'child_process'; import { promisify } from 'util'; +import { getAllDescendantPids as getDescendantPids } from '../utils/processUtils'; interface TerminalSession { pty: pty.IPty; @@ -154,49 +155,8 @@ export class TerminalSessionManager extends EventEmitter { * This is critical for ensuring all child processes are killed */ private getAllDescendantPids(parentPid: number): number[] { - const descendants: number[] = []; - const platform = os.platform(); - - try { - if (platform === 'win32') { - // Windows: Use WMIC to get child processes - const result = require('child_process').execSync( - `wmic process where (ParentProcessId=${parentPid}) get ProcessId`, - { encoding: 'utf8' } - ); - - const lines = result.split('\n').filter((line: string) => line.trim()); - for (let i = 1; i < lines.length; i++) { // Skip header - const pid = parseInt(lines[i].trim()); - if (!isNaN(pid) && pid !== parentPid) { - descendants.push(pid); - // Recursively get children of this process - descendants.push(...this.getAllDescendantPids(pid)); - } - } - } else { - // Unix/Linux/macOS: Use ps command - const result = require('child_process').execSync( - `ps -o pid= --ppid ${parentPid} 2>/dev/null || true`, - { encoding: 'utf8' } - ); - - const pids = result.split('\n') - .map((line: string) => parseInt(line.trim())) - .filter((pid: number) => !isNaN(pid) && pid !== parentPid); - - for (const pid of pids) { - descendants.push(pid); - // Recursively get children of this process - descendants.push(...this.getAllDescendantPids(pid)); - } - } - } catch (error) { - console.warn(`Error getting descendant PIDs for ${parentPid}:`, error); - } - - // Remove duplicates - return [...new Set(descendants)]; + // Use the shared utility function that handles Windows 11 compatibility + return getDescendantPids(parentPid); } /** diff --git a/main/src/types/config.ts b/main/src/types/config.ts index 515e151b..c95f8dfc 100644 --- a/main/src/types/config.ts +++ b/main/src/types/config.ts @@ -1,5 +1,6 @@ export interface AppConfig { verbose?: boolean; + debugMode?: boolean; anthropicApiKey?: string; // Legacy fields for backward compatibility gitRepoPath?: string; @@ -30,6 +31,7 @@ export interface AppConfig { export interface UpdateConfigRequest { verbose?: boolean; + debugMode?: boolean; anthropicApiKey?: string; claudeExecutablePath?: string; systemPromptAppend?: string; diff --git a/main/src/utils/consoleWrapper.ts b/main/src/utils/consoleWrapper.ts index 4408774b..4016574f 100644 --- a/main/src/utils/consoleWrapper.ts +++ b/main/src/utils/consoleWrapper.ts @@ -1,6 +1,8 @@ -// Simple console wrapper to reduce logging in production +// Simple console wrapper to reduce logging based on verbose setting // This follows the existing pattern in the codebase +import { ConfigManager } from '../services/configManager'; + const isDevelopment = process.env.NODE_ENV !== 'production' && !(global as any).isPackaged; // Store original console methods @@ -12,17 +14,49 @@ const originalConsole = { debug: console.debug }; +// ConfigManager instance will be set after initialization +let configManager: ConfigManager | null = null; + +// Helper to check if verbose logging is enabled +function isVerboseEnabled(): boolean { + // If ConfigManager is not yet initialized, check for verbose in stored config + if (!configManager) { + try { + // Try to read the config file directly during startup + const Store = require('electron-store'); + const store = new Store({ name: 'crystal-settings' }); + const config = store.get('config', {}); + return config.verbose || false; + } catch { + // If we can't read config, default to NOT verbose (shut up the logs!) + return false; + } + } + + try { + const config = configManager.getConfig(); + return config?.verbose || false; + } catch { + // If config can't be read, default to not verbose + return false; + } +} + // Helper to check if a message should be logged function shouldLog(level: 'log' | 'info' | 'debug', args: any[]): boolean { if (args.length === 0) return false; + const firstArg = args[0]; if (typeof firstArg === 'string') { - // Always log [Main] messages as they're important startup info - if (firstArg.includes('[Main]')) return true; // Always log errors from any component if (firstArg.includes('Error') || firstArg.includes('Failed')) return true; - // Skip verbose logging from these components in both dev and production + // If verbose is disabled, block EVERYTHING except errors + if (!isVerboseEnabled()) { + return false; // Block ALL non-error logs when verbose is off + } + + // When verbose is enabled, still skip these extremely noisy logs if (firstArg.includes('[CommandExecutor]')) return false; if (firstArg.includes('[ShellPath]')) return false; if (firstArg.includes('[Database] Getting folders')) return false; @@ -40,13 +74,11 @@ function shouldLog(level: 'log' | 'info' | 'debug', args: any[]): boolean { if (firstArg.includes('[IPC:git] Project path:')) return false; if (firstArg.includes('[IPC:git] Using main branch:')) return false; - // In development, log everything else - if (isDevelopment) { - return true; - } + // Log everything else when verbose is enabled + return true; } - return !isDevelopment; // In production, default to not logging + return isVerboseEnabled(); // Default to verbose setting } // Override console methods @@ -74,5 +106,10 @@ export function setupConsoleWrapper() { console.error = originalConsole.error; } +// Function to update the config manager after it's initialized +export function updateConfigManager(cm: ConfigManager) { + configManager = cm; +} + // Export original console for critical logging export { originalConsole }; \ No newline at end of file diff --git a/main/src/utils/processUtils.ts b/main/src/utils/processUtils.ts new file mode 100644 index 00000000..eb775bd5 --- /dev/null +++ b/main/src/utils/processUtils.ts @@ -0,0 +1,137 @@ +import { execSync } from 'child_process'; +import * as os from 'os'; + +/** + * Gets all child process IDs for a given parent process ID. + * Works on both Windows (using PowerShell/wmic) and Unix systems. + */ +export function getChildProcessIds(parentPid: number): number[] { + const children: number[] = []; + const platform = os.platform(); + + try { + if (platform === 'win32') { + // Windows: Use PowerShell to get child processes (wmic is deprecated in Windows 11) + try { + // First try PowerShell (works on all modern Windows) + const result = execSync( + `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty ProcessId"`, + { encoding: 'utf8', windowsHide: true } + ); + + const pids = result.split('\n') + .map((line: string) => parseInt(line.trim())) + .filter((pid: number) => !isNaN(pid) && pid !== parentPid); + + children.push(...pids); + } catch (psError) { + // Fallback to wmic for older Windows versions + try { + const result = execSync( + `wmic process where (ParentProcessId=${parentPid}) get ProcessId`, + { encoding: 'utf8', windowsHide: true } + ); + + const lines = result.split('\n').filter((line: string) => line.trim()); + for (let i = 1; i < lines.length; i++) { // Skip header + const pid = parseInt(lines[i].trim()); + if (!isNaN(pid) && pid !== parentPid) { + children.push(pid); + } + } + } catch (wmicError) { + // If both fail, just log and continue (process might already be gone) + console.warn(`Could not get child processes for PID ${parentPid} (process may have already exited)`); + } + } + } else { + // Unix/Linux/macOS: Use ps command + const result = execSync( + `ps -o pid= --ppid ${parentPid} 2>/dev/null || true`, + { encoding: 'utf8' } + ); + + const pids = result.split('\n') + .map((line: string) => parseInt(line.trim())) + .filter((pid: number) => !isNaN(pid) && pid !== parentPid); + + children.push(...pids); + } + } catch (error) { + console.warn(`Error getting child PIDs for ${parentPid}:`, error); + } + + return children; +} + +/** + * Gets all descendant process IDs (children, grandchildren, etc.) recursively. + */ +export function getAllDescendantPids(parentPid: number): number[] { + const descendants: number[] = []; + const children = getChildProcessIds(parentPid); + + for (const childPid of children) { + descendants.push(childPid); + // Recursively get descendants of this child + descendants.push(...getAllDescendantPids(childPid)); + } + + // Remove duplicates + return [...new Set(descendants)]; +} + +/** + * Gets the process name for a given PID. + * Returns null if the process doesn't exist or can't be accessed. + */ +export function getProcessName(pid: number): string | null { + const platform = os.platform(); + + try { + if (platform === 'win32') { + // Windows: Use PowerShell to get process name (wmic is deprecated in Windows 11) + try { + // First try PowerShell (works on all modern Windows) + const result = execSync( + `powershell -NoProfile -Command "Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name"`, + { encoding: 'utf8', windowsHide: true } + ); + + const name = result.trim(); + return name || null; + } catch (psError) { + // Fallback to wmic for older Windows versions + try { + const result = execSync( + `wmic process where ProcessId=${pid} get Name`, + { encoding: 'utf8', windowsHide: true } + ); + + const lines = result.split('\n').filter((line: string) => line.trim()); + if (lines.length > 1) { + // Skip the header line and get the process name + return lines[1].trim() || null; + } + } catch (wmicError) { + // Process doesn't exist or can't be accessed + return null; + } + } + } else { + // Unix/Linux/macOS: Use ps command + const result = execSync( + `ps -p ${pid} -o comm= 2>/dev/null || true`, + { encoding: 'utf8' } + ); + + const name = result.trim(); + return name || null; + } + } catch (error) { + // Process doesn't exist or can't be accessed + return null; + } + + return null; +} \ No newline at end of file diff --git a/main/src/utils/shellDetector.ts b/main/src/utils/shellDetector.ts index 50fc4043..d959a108 100644 --- a/main/src/utils/shellDetector.ts +++ b/main/src/utils/shellDetector.ts @@ -13,21 +13,13 @@ interface ShellInfo { * Detects the user's default shell in a robust, cross-platform way */ export class ShellDetector { - private static cachedShell: ShellInfo | null = null; - /** * Get the user's default shell - * @param forceRefresh Force re-detection instead of using cache + * @param forceRefresh Ignored - kept for compatibility * @returns Shell information including path and name */ static getDefaultShell(forceRefresh = false): ShellInfo { - if (!forceRefresh && this.cachedShell) { - return this.cachedShell; - } - - const shell = this.detectShell(); - this.cachedShell = shell; - return shell; + return this.detectShell(); } private static detectShell(): ShellInfo { @@ -41,26 +33,36 @@ export class ShellDetector { } private static detectWindowsShell(): ShellInfo { - // Check for PowerShell Core first - const pwshPath = this.findExecutable('pwsh.exe'); - if (pwshPath) { - return { path: pwshPath, name: 'pwsh' }; + // First try PowerShell Core (pwsh.exe) - the modern PowerShell + const pwshPaths = [ + path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'PowerShell', '7', 'pwsh.exe'), + path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'PowerShell', '6', 'pwsh.exe'), + 'C:\\Program Files\\PowerShell\\7\\pwsh.exe', + 'C:\\Program Files\\PowerShell\\6\\pwsh.exe' + ]; + + for (const pwshPath of pwshPaths) { + if (fs.existsSync(pwshPath)) { + return { path: pwshPath, name: 'pwsh' }; + } } - - // Check for Windows PowerShell - const powershellPath = this.findExecutable('powershell.exe'); - if (powershellPath) { + + // Fall back to Windows PowerShell if Core isn't installed + const powershellPath = path.join( + process.env.SYSTEMROOT || 'C:\\Windows', + 'System32', + 'WindowsPowerShell', + 'v1.0', + 'powershell.exe' + ); + + if (fs.existsSync(powershellPath)) { return { path: powershellPath, name: 'powershell' }; } - - // Fall back to cmd.exe + + // Last resort: cmd.exe const cmdPath = path.join(process.env.SYSTEMROOT || 'C:\\Windows', 'System32', 'cmd.exe'); - if (fs.existsSync(cmdPath)) { - return { path: cmdPath, name: 'cmd' }; - } - - // Last resort - return { path: 'cmd.exe', name: 'cmd' }; + return { path: cmdPath, name: 'cmd' }; } private static detectUnixShell(): ShellInfo { @@ -139,10 +141,17 @@ export class ShellDetector { const fullPath = path.join(dir, name); if (fs.existsSync(fullPath)) { try { - fs.accessSync(fullPath, fs.constants.X_OK); - return fullPath; + // On Windows, fs.constants.X_OK doesn't work properly + // Just check if the file exists + if (process.platform === 'win32') { + fs.accessSync(fullPath, fs.constants.F_OK); + return fullPath; + } else { + fs.accessSync(fullPath, fs.constants.X_OK); + return fullPath; + } } catch { - // Not executable, continue searching + // Not accessible, continue searching } } } @@ -193,7 +202,13 @@ export class ShellDetector { */ static isShellAvailable(shellPath: string): boolean { try { - fs.accessSync(shellPath, fs.constants.X_OK); + // On Windows, fs.constants.X_OK doesn't work properly + // Just check if the file exists + if (process.platform === 'win32') { + fs.accessSync(shellPath, fs.constants.F_OK); + } else { + fs.accessSync(shellPath, fs.constants.X_OK); + } return true; } catch { return false; diff --git a/main/src/utils/shellEscape.ts b/main/src/utils/shellEscape.ts index c78afc8c..7278eb5b 100644 --- a/main/src/utils/shellEscape.ts +++ b/main/src/utils/shellEscape.ts @@ -11,12 +11,17 @@ export function escapeShellArg(arg: string): string { // If the argument is empty, return empty quotes if (!arg) return "''"; - // For Windows, wrap in double quotes and escape internal quotes + // For Windows, we need to handle both cmd.exe and PowerShell + // The safest approach is to use double quotes and escape properly if (process.platform === 'win32') { - // Escape existing double quotes and backslashes - const escaped = arg - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"'); + // For Windows, we need to escape quotes and special characters + // This works for both cmd.exe and PowerShell when executed through exec() + let escaped = arg; + + // First, escape any existing double quotes by doubling them (works for both shells) + escaped = escaped.replace(/"/g, '""'); + + // Wrap the whole thing in double quotes return `"${escaped}"`; } diff --git a/tests/setup.ts b/tests/setup.ts index 4d1f0ca6..f0e14c5f 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -13,7 +13,10 @@ export async function setupTestProject() { execSync('git init -b main', { cwd: testProjectPath, stdio: 'pipe' }); execSync('git config user.email "test@example.com"', { cwd: testProjectPath, stdio: 'pipe' }); execSync('git config user.name "Test User"', { cwd: testProjectPath, stdio: 'pipe' }); - execSync('touch README.md', { cwd: testProjectPath }); + + // Create README.md using Node.js fs instead of touch (Windows compatible) + fs.writeFileSync(path.join(testProjectPath, 'README.md'), '# Test Project\n'); + execSync('git add .', { cwd: testProjectPath }); execSync('git commit -m "Initial commit"', { cwd: testProjectPath }); @@ -21,6 +24,12 @@ export async function setupTestProject() { } export async function cleanupTestProject(projectPath: string) { + // Guard against undefined path + if (!projectPath) { + console.warn('Cannot cleanup test project: path is undefined'); + return; + } + try { fs.rmSync(projectPath, { recursive: true, force: true }); } catch (error) { From 16eb0fea4691c1963af74e7a943b0d2f4a458f8b Mon Sep 17 00:00:00 2001 From: Jordan Bentley Date: Tue, 12 Aug 2025 11:31:13 -0400 Subject: [PATCH 12/14] checkpoint: https://github.com/stravu/crystal/pull/145 Can... --- frontend/src/hooks/useSessionView.ts | 2 +- frontend/src/types/electron.d.ts | 86 +++++++++------- main/src/autoUpdater.ts | 7 +- main/src/database/database.ts | 140 +++++++++++++++------------ 4 files changed, 138 insertions(+), 97 deletions(-) diff --git a/frontend/src/hooks/useSessionView.ts b/frontend/src/hooks/useSessionView.ts index 10dc2b6d..56b03543 100644 --- a/frontend/src/hooks/useSessionView.ts +++ b/frontend/src/hooks/useSessionView.ts @@ -1471,7 +1471,7 @@ export const useSessionView = ( try { await API.sessions.rename(activeSession.id, editName.trim()); setIsEditingName(false); - } catch (error) { + } catch { alert('Failed to rename session'); setEditName(activeSession.name); setIsEditingName(false); diff --git a/frontend/src/types/electron.d.ts b/frontend/src/types/electron.d.ts index 2b1ea44d..7d19fcc3 100644 --- a/frontend/src/types/electron.d.ts +++ b/frontend/src/types/electron.d.ts @@ -1,6 +1,10 @@ // Type definitions for Electron preload API -interface IPCResponse { +import type { Session, CreateSessionRequest, SessionOutput, GitStatus } from './session'; +import type { Project, CreateProjectRequest, UpdateProjectRequest } from './project'; +import type { Folder } from './folder'; + +interface IPCResponse { success: boolean; data?: T; error?: string; @@ -10,7 +14,7 @@ interface IPCResponse { interface ElectronAPI { // Generic invoke method for direct IPC calls - invoke: (channel: string, ...args: any[]) => Promise; + invoke: (channel: string, ...args: unknown[]) => Promise; // Basic app info getAppVersion: () => Promise; @@ -37,7 +41,7 @@ interface ElectronAPI { getAllWithProjects: () => Promise; getArchivedWithProjects: () => Promise; get: (sessionId: string) => Promise; - create: (request: any) => Promise; + create: (request: CreateSessionRequest) => Promise>; delete: (sessionId: string) => Promise; sendInput: (sessionId: string, input: string) => Promise; continue: (sessionId: string, prompt?: string, model?: string) => Promise; @@ -109,9 +113,9 @@ interface ElectronAPI { projects: { getAll: () => Promise; getActive: () => Promise; - create: (projectData: any) => Promise; + create: (projectData: CreateProjectRequest) => Promise>; activate: (projectId: string) => Promise; - update: (projectId: string, updates: any) => Promise; + update: (projectId: string, updates: UpdateProjectRequest) => Promise>; delete: (projectId: string) => Promise; detectBranch: (path: string) => Promise; reorder: (projectOrders: Array<{ id: number; displayOrder: number }>) => Promise; @@ -138,7 +142,7 @@ interface ElectronAPI { // Configuration config: { get: () => Promise; - update: (updates: any) => Promise; + update: (updates: Record) => Promise; }; // Prompts @@ -155,13 +159,13 @@ interface ElectronAPI { // Dialog dialog: { - openFile: (options?: any) => Promise>; - openDirectory: (options?: any) => Promise>; + openFile: (options?: Electron.OpenDialogOptions) => Promise>; + openDirectory: (options?: Electron.OpenDialogOptions) => Promise>; }; // Permissions permissions: { - respond: (requestId: string, response: any) => Promise; + respond: (requestId: string, response: { approved: boolean; data?: unknown }) => Promise; getPending: () => Promise; }; @@ -180,8 +184,8 @@ interface ElectronAPI { dashboard: { getProjectStatus: (projectId: number) => Promise; getProjectStatusProgressive: (projectId: number) => Promise; - onUpdate: (callback: (data: any) => void) => () => void; - onSessionUpdate: (callback: (data: any) => void) => () => void; + onUpdate: (callback: (data: unknown) => void) => () => void; + onSessionUpdate: (callback: (data: { sessionId: string; status?: string; progress?: number }) => void) => () => void; }; // UI State management @@ -194,36 +198,36 @@ interface ElectronAPI { // Event listeners for real-time updates events: { - onSessionCreated: (callback: (session: any) => void) => () => void; - onSessionUpdated: (callback: (session: any) => void) => () => void; - onSessionDeleted: (callback: (session: any) => void) => () => void; - onSessionsLoaded: (callback: (sessions: any[]) => void) => () => void; - onSessionOutput: (callback: (output: any) => void) => () => void; - onSessionOutputAvailable: (callback: (info: any) => void) => () => void; - onGitStatusUpdated: (callback: (data: { sessionId: string; gitStatus: any }) => void) => () => void; + onSessionCreated: (callback: (session: Session) => void) => () => void; + onSessionUpdated: (callback: (session: Session) => void) => () => void; + onSessionDeleted: (callback: (session: Session) => void) => () => void; + onSessionsLoaded: (callback: (sessions: Session[]) => void) => () => void; + onSessionOutput: (callback: (output: SessionOutput) => void) => () => void; + onSessionOutputAvailable: (callback: (info: { sessionId: string; available: boolean }) => void) => () => void; + onGitStatusUpdated: (callback: (data: { sessionId: string; gitStatus: GitStatus }) => void) => () => void; onGitStatusLoading: (callback: (data: { sessionId: string }) => void) => () => void; onGitStatusLoadingBatch?: (callback: (sessionIds: string[]) => void) => () => void; - onGitStatusUpdatedBatch?: (callback: (updates: Array<{ sessionId: string; status: any }>) => void) => () => void; + onGitStatusUpdatedBatch?: (callback: (updates: Array<{ sessionId: string; status: GitStatus }>) => void) => () => void; // Project events - onProjectUpdated: (callback: (project: any) => void) => () => void; + onProjectUpdated: (callback: (project: Project) => void) => () => void; // Folder events - onFolderCreated: (callback: (folder: any) => void) => () => void; - onFolderUpdated: (callback: (folder: any) => void) => () => void; + onFolderCreated: (callback: (folder: Folder) => void) => () => void; + onFolderUpdated: (callback: (folder: Folder) => void) => () => void; onFolderDeleted: (callback: (folderId: string) => void) => () => void; - onScriptOutput: (callback: (output: any) => void) => () => void; + onScriptOutput: (callback: (output: { type: string; data: string }) => void) => () => void; onMainLog: (callback: (level: string, message: string) => void) => () => void; - onVersionUpdateAvailable: (callback: (versionInfo: any) => void) => () => void; + onVersionUpdateAvailable: (callback: (versionInfo: { current: string; latest: string; updateAvailable: boolean }) => void) => () => void; // Auto-updater events onUpdaterCheckingForUpdate: (callback: () => void) => () => void; - onUpdaterUpdateAvailable: (callback: (info: any) => void) => () => void; - onUpdaterUpdateNotAvailable: (callback: (info: any) => void) => () => void; - onUpdaterDownloadProgress: (callback: (progressInfo: any) => void) => () => void; - onUpdaterUpdateDownloaded: (callback: (info: any) => void) => () => void; - onUpdaterError: (callback: (error: any) => void) => () => void; + onUpdaterUpdateAvailable: (callback: (info: { version: string; releaseNotes?: string }) => void) => () => void; + onUpdaterUpdateNotAvailable: (callback: (info: { version: string }) => void) => () => void; + onUpdaterDownloadProgress: (callback: (progressInfo: { percent: number; bytesPerSecond: number; total?: number; transferred?: number }) => void) => () => void; + onUpdaterUpdateDownloaded: (callback: (info: { version: string; releaseNotes?: string }) => void) => () => void; + onUpdaterError: (callback: (error: { message: string; stack?: string }) => void) => () => void; // Process management events onZombieProcessesDetected: (callback: (data: { sessionId?: string | null; pids?: number[]; message: string }) => void) => () => void; @@ -239,7 +243,7 @@ interface ElectronAPI { name: string; type: string; notnull: number; - dflt_value: any; + dflt_value: string | number | null; pk: number; }>; foreignKeys: Array<{ @@ -261,12 +265,28 @@ interface ElectronAPI { }; } +// Define Electron dialog options if not already available +interface ElectronOpenDialogOptions { + title?: string; + defaultPath?: string; + buttonLabel?: string; + filters?: Array<{ name: string; extensions: string[] }>; + properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles' | 'createDirectory' | 'promptToCreate' | 'noResolveAliases' | 'treatPackageAsDirectory'>; + message?: string; + securityScopedBookmarks?: boolean; +} + +// Use global Electron type if available, otherwise use our definition +type Electron = { + OpenDialogOptions: ElectronOpenDialogOptions; +} + // Additional electron interface for IPC event listeners interface ElectronInterface { openExternal: (url: string) => Promise; - invoke: (channel: string, ...args: any[]) => Promise; - on: (channel: string, callback: (...args: any[]) => void) => void; - off: (channel: string, callback: (...args: any[]) => void) => void; + invoke: (channel: string, ...args: unknown[]) => Promise; + on: (channel: string, callback: (...args: unknown[]) => void) => void; + off: (channel: string, callback: (...args: unknown[]) => void) => void; } declare global { diff --git a/main/src/autoUpdater.ts b/main/src/autoUpdater.ts index 23d106d9..4ec9e47c 100644 --- a/main/src/autoUpdater.ts +++ b/main/src/autoUpdater.ts @@ -10,8 +10,11 @@ export function setupAutoUpdater(getMainWindow: () => BrowserWindow | null): voi // TEST MODE: Use local server for testing if (process.env.TEST_UPDATES === 'true') { - const { setupTestUpdater } = require('./test-updater'); - setupTestUpdater(); + // Dynamic import to avoid ESLint warning + void (async () => { + const { setupTestUpdater } = await import('./test-updater'); + setupTestUpdater(); + })(); console.log('[AutoUpdater] Using test update server at:', process.env.UPDATE_SERVER_URL || 'http://localhost:8080'); } else { // Configure electron-updater for production diff --git a/main/src/database/database.ts b/main/src/database/database.ts index 629ecc6c..78ae4059 100644 --- a/main/src/database/database.ts +++ b/main/src/database/database.ts @@ -3,6 +3,21 @@ import { readFileSync, mkdirSync } from 'fs'; import { join, dirname, basename } from 'path'; import type { Project, ProjectRunCommand, Folder, Session, SessionOutput, CreateSessionData, UpdateSessionData, ConversationMessage, PromptMarker, ExecutionDiff, CreateExecutionDiffData } from './models'; +// Type for SQLite PRAGMA table_info result +interface TableColumn { + cid: number; + name: string; + type: string; + notnull: number; + dflt_value: string | null; + pk: number; +} + +// Type for SQLite master table query result +interface TableInfo { + name: string; +} + export class DatabaseService { private db: Database.Database; @@ -35,9 +50,9 @@ export class DatabaseService { private runMigrations(): void { // Check if archived column exists const tableInfo = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasArchivedColumn = tableInfo.some((col: any) => col.name === 'archived'); - const hasInitialPromptColumn = tableInfo.some((col: any) => col.name === 'initial_prompt'); - const hasLastViewedAtColumn = tableInfo.some((col: any) => col.name === 'last_viewed_at'); + const hasArchivedColumn = tableInfo.some((col: TableColumn) => col.name === 'archived'); + const hasInitialPromptColumn = tableInfo.some((col: TableColumn) => col.name === 'initial_prompt'); + const hasLastViewedAtColumn = tableInfo.some((col: TableColumn) => col.name === 'last_viewed_at'); if (!hasArchivedColumn) { // Run migration to add archived column @@ -47,7 +62,7 @@ export class DatabaseService { // Check if we need to rename prompt to initial_prompt if (!hasInitialPromptColumn) { - const hasPromptColumn = tableInfo.some((col: any) => col.name === 'prompt'); + const hasPromptColumn = tableInfo.some((col: TableColumn) => col.name === 'prompt'); if (hasPromptColumn) { this.db.prepare("ALTER TABLE sessions RENAME COLUMN prompt TO initial_prompt").run(); } @@ -89,8 +104,8 @@ export class DatabaseService { } else { // Check if the table has the correct column name const promptMarkersInfo = this.db.prepare("PRAGMA table_info(prompt_markers)").all(); - const hasOutputLineColumn = promptMarkersInfo.some((col: any) => col.name === 'output_line'); - const hasTerminalLineColumn = promptMarkersInfo.some((col: any) => col.name === 'terminal_line'); + const hasOutputLineColumn = promptMarkersInfo.some((col: TableColumn) => col.name === 'output_line'); + const hasTerminalLineColumn = promptMarkersInfo.some((col: TableColumn) => col.name === 'terminal_line'); if (hasTerminalLineColumn && !hasOutputLineColumn) { // Rename the column from terminal_line to output_line @@ -134,14 +149,14 @@ export class DatabaseService { // Add commit_message column to execution_diffs if it doesn't exist const executionDiffsTableInfo = this.db.prepare("PRAGMA table_info(execution_diffs)").all(); - const hasCommitMessageColumn = executionDiffsTableInfo.some((col: any) => col.name === 'commit_message'); + const hasCommitMessageColumn = executionDiffsTableInfo.some((col: TableColumn) => col.name === 'commit_message'); if (!hasCommitMessageColumn) { this.db.prepare("ALTER TABLE execution_diffs ADD COLUMN commit_message TEXT").run(); } // Check if claude_session_id column exists const sessionTableInfoClaude = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasClaudeSessionIdColumn = sessionTableInfoClaude.some((col: any) => col.name === 'claude_session_id'); + const hasClaudeSessionIdColumn = sessionTableInfoClaude.some((col: TableColumn) => col.name === 'claude_session_id'); if (!hasClaudeSessionIdColumn) { // Add claude_session_id column to store Claude's actual session ID @@ -149,7 +164,7 @@ export class DatabaseService { } // Check if permission_mode column exists - const hasPermissionModeColumn = sessionTableInfoClaude.some((col: any) => col.name === 'permission_mode'); + const hasPermissionModeColumn = sessionTableInfoClaude.some((col: TableColumn) => col.name === 'permission_mode'); if (!hasPermissionModeColumn) { // Add permission_mode column to sessions table @@ -175,7 +190,7 @@ export class DatabaseService { // Add project_id to sessions table const sessionsTableInfoProjects = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasProjectIdColumn = sessionsTableInfoProjects.some((col: any) => col.name === 'project_id'); + const hasProjectIdColumn = sessionsTableInfoProjects.some((col: TableColumn) => col.name === 'project_id'); if (!hasProjectIdColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE").run(); @@ -184,7 +199,8 @@ export class DatabaseService { // Import existing config as default project if it exists try { - const configManager = require('../services/configManager').configManager; + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { configManager } = require('../services/configManager') as typeof import('../services/configManager'); const gitRepoPath = configManager.getGitRepoPath(); if (gitRepoPath) { @@ -211,7 +227,7 @@ export class DatabaseService { // Add is_main_repo column to sessions table if it doesn't exist const sessionTableInfoForMainRepo = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasIsMainRepoColumn = sessionTableInfoForMainRepo.some((col: any) => col.name === 'is_main_repo'); + const hasIsMainRepoColumn = sessionTableInfoForMainRepo.some((col: TableColumn) => col.name === 'is_main_repo'); if (!hasIsMainRepoColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN is_main_repo BOOLEAN DEFAULT 0").run(); @@ -220,28 +236,28 @@ export class DatabaseService { // Add main_branch column to projects table if it doesn't exist const projectsTableInfo = this.db.prepare("PRAGMA table_info(projects)").all(); - const hasMainBranchColumn = projectsTableInfo.some((col: any) => col.name === 'main_branch'); + const hasMainBranchColumn = projectsTableInfo.some((col: TableColumn) => col.name === 'main_branch'); if (!hasMainBranchColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN main_branch TEXT").run(); } // Add build_script column to projects table if it doesn't exist - const hasBuildScriptColumn = projectsTableInfo.some((col: any) => col.name === 'build_script'); + const hasBuildScriptColumn = projectsTableInfo.some((col: TableColumn) => col.name === 'build_script'); if (!hasBuildScriptColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN build_script TEXT").run(); } // Add default_permission_mode column to projects table if it doesn't exist - const hasDefaultPermissionModeColumn = projectsTableInfo.some((col: any) => col.name === 'default_permission_mode'); + const hasDefaultPermissionModeColumn = projectsTableInfo.some((col: TableColumn) => col.name === 'default_permission_mode'); if (!hasDefaultPermissionModeColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN default_permission_mode TEXT DEFAULT 'ignore' CHECK(default_permission_mode IN ('approve', 'ignore'))").run(); } // Add open_ide_command column to projects table if it doesn't exist - const hasOpenIdeCommandColumn = projectsTableInfo.some((col: any) => col.name === 'open_ide_command'); + const hasOpenIdeCommandColumn = projectsTableInfo.some((col: TableColumn) => col.name === 'open_ide_command'); if (!hasOpenIdeCommandColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN open_ide_command TEXT").run(); @@ -278,8 +294,8 @@ export class DatabaseService { // Check if display_order columns exist const projectsTableInfo2 = this.db.prepare("PRAGMA table_info(projects)").all(); const sessionsTableInfo2 = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasProjectsDisplayOrder = projectsTableInfo2.some((col: any) => col.name === 'display_order'); - const hasSessionsDisplayOrder = sessionsTableInfo2.some((col: any) => col.name === 'display_order'); + const hasProjectsDisplayOrder = projectsTableInfo2.some((col: TableColumn) => col.name === 'display_order'); + const hasSessionsDisplayOrder = sessionsTableInfo2.some((col: TableColumn) => col.name === 'display_order'); if (!hasProjectsDisplayOrder) { // Add display_order to projects @@ -321,18 +337,18 @@ export class DatabaseService { // Normalize timestamp fields migration // Check if last_viewed_at is still TEXT type const sessionTableInfoTimestamp = this.db.prepare("PRAGMA table_info(sessions)").all(); - const lastViewedAtColumn = sessionTableInfoTimestamp.find((col: any) => col.name === 'last_viewed_at') as any; + const lastViewedAtColumn = sessionTableInfoTimestamp.find((col: TableColumn) => col.name === 'last_viewed_at') as TableColumn | undefined; // Skip this migration if last_viewed_at_new already exists (migration partially completed) - const hasLastViewedAtNew = sessionTableInfoTimestamp.some((col: any) => col.name === 'last_viewed_at_new'); + const hasLastViewedAtNew = sessionTableInfoTimestamp.some((col: TableColumn) => col.name === 'last_viewed_at_new'); if (lastViewedAtColumn && lastViewedAtColumn.type === 'TEXT' && !hasLastViewedAtNew) { console.log('[Database] Running timestamp normalization migration...'); try { // Check if the new columns already exist (from a previous failed migration) - const hasLastViewedAtNew = sessionTableInfoTimestamp.some((col: any) => col.name === 'last_viewed_at_new'); - const hasRunStartedAtNew = sessionTableInfoTimestamp.some((col: any) => col.name === 'run_started_at_new'); + const hasLastViewedAtNew = sessionTableInfoTimestamp.some((col: TableColumn) => col.name === 'last_viewed_at_new'); + const hasRunStartedAtNew = sessionTableInfoTimestamp.some((col: TableColumn) => col.name === 'run_started_at_new'); // Create new temporary columns with DATETIME type if they don't exist if (!hasLastViewedAtNew) { @@ -400,7 +416,7 @@ export class DatabaseService { // Add missing completion_timestamp to prompt_markers if it doesn't exist const promptMarkersInfo = this.db.prepare("PRAGMA table_info(prompt_markers)").all(); - const hasCompletionTimestamp = promptMarkersInfo.some((col: any) => col.name === 'completion_timestamp'); + const hasCompletionTimestamp = promptMarkersInfo.some((col: TableColumn) => col.name === 'completion_timestamp'); if (!hasCompletionTimestamp) { this.db.prepare("ALTER TABLE prompt_markers ADD COLUMN completion_timestamp DATETIME").run(); @@ -408,7 +424,7 @@ export class DatabaseService { // Add is_favorite column to sessions table if it doesn't exist const sessionTableInfoFavorite = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasIsFavoriteColumn = sessionTableInfoFavorite.some((col: any) => col.name === 'is_favorite'); + const hasIsFavoriteColumn = sessionTableInfoFavorite.some((col: TableColumn) => col.name === 'is_favorite'); if (!hasIsFavoriteColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN is_favorite BOOLEAN DEFAULT 0").run(); @@ -416,7 +432,7 @@ export class DatabaseService { } // Add auto_commit column to sessions table if it doesn't exist - const hasAutoCommitColumn = sessionTableInfoFavorite.some((col: any) => col.name === 'auto_commit'); + const hasAutoCommitColumn = sessionTableInfoFavorite.some((col: TableColumn) => col.name === 'auto_commit'); if (!hasAutoCommitColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN auto_commit BOOLEAN DEFAULT 1").run(); @@ -424,7 +440,7 @@ export class DatabaseService { } // Add skip_continue_next column to sessions table if it doesn't exist - const hasSkipContinueNextColumn = sessionTableInfoFavorite.some((col: any) => col.name === 'skip_continue_next'); + const hasSkipContinueNextColumn = sessionTableInfoFavorite.some((col: TableColumn) => col.name === 'skip_continue_next'); if (!hasSkipContinueNextColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN skip_continue_next BOOLEAN DEFAULT 0").run(); @@ -442,7 +458,7 @@ export class DatabaseService { // Check if the old folders table has INTEGER id if (foldersExists) { const foldersInfo = this.db.prepare("PRAGMA table_info(folders)").all(); - const idColumn = foldersInfo.find((col: any) => col.name === 'id') as any; + const idColumn = foldersInfo.find((col: TableColumn) => col.name === 'id') as TableColumn | undefined; if (idColumn && idColumn.type === 'INTEGER') { // Old folders table with INTEGER id exists, drop it @@ -489,7 +505,7 @@ export class DatabaseService { // Update sessions table folder_id column type if needed const sessionTableInfo = this.db.prepare("PRAGMA table_info(sessions)").all(); - const folderIdColumn = sessionTableInfo.find((col: any) => col.name === 'folder_id') as any; + const folderIdColumn = sessionTableInfo.find((col: TableColumn) => col.name === 'folder_id') as TableColumn | undefined; if (folderIdColumn && folderIdColumn.type === 'INTEGER') { console.log('[Database] Converting sessions.folder_id from INTEGER to TEXT...'); @@ -569,7 +585,7 @@ export class DatabaseService { `).run(); // Add folder_id column to sessions table if it doesn't exist - const hasFolderIdColumn = sessionTableInfoFavorite.some((col: any) => col.name === 'folder_id'); + const hasFolderIdColumn = sessionTableInfoFavorite.some((col: TableColumn) => col.name === 'folder_id'); if (!hasFolderIdColumn) { this.db.prepare('ALTER TABLE sessions ADD COLUMN folder_id TEXT REFERENCES folders(id) ON DELETE SET NULL').run(); @@ -584,7 +600,7 @@ export class DatabaseService { // Add parent_folder_id column to folders table for nested folders support const foldersTableInfo = this.db.prepare("PRAGMA table_info(folders)").all(); - const hasParentFolderIdColumn = foldersTableInfo.some((col: any) => col.name === 'parent_folder_id'); + const hasParentFolderIdColumn = foldersTableInfo.some((col: TableColumn) => col.name === 'parent_folder_id'); if (!hasParentFolderIdColumn) { this.db.prepare('ALTER TABLE folders ADD COLUMN parent_folder_id TEXT REFERENCES folders(id) ON DELETE CASCADE').run(); @@ -629,7 +645,7 @@ export class DatabaseService { // Add model column to sessions table if it doesn't exist const sessionTableInfoModel = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasModelColumn = sessionTableInfoModel.some((col: any) => col.name === 'model'); + const hasModelColumn = sessionTableInfoModel.some((col: TableColumn) => col.name === 'model'); if (!hasModelColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN model TEXT DEFAULT 'claude-sonnet-4-20250514'").run(); @@ -677,7 +693,7 @@ export class DatabaseService { // Add worktree_folder column to projects table if it doesn't exist const projectsTableInfoWorktree = this.db.prepare("PRAGMA table_info(projects)").all(); - const hasWorktreeFolderColumn = projectsTableInfoWorktree.some((col: any) => col.name === 'worktree_folder'); + const hasWorktreeFolderColumn = projectsTableInfoWorktree.some((col: TableColumn) => col.name === 'worktree_folder'); if (!hasWorktreeFolderColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN worktree_folder TEXT").run(); @@ -686,7 +702,7 @@ export class DatabaseService { // Add lastUsedModel column to projects table if it doesn't exist const projectsTableInfoModel = this.db.prepare("PRAGMA table_info(projects)").all(); - const hasLastUsedModelColumn = projectsTableInfoModel.some((col: any) => col.name === 'lastUsedModel'); + const hasLastUsedModelColumn = projectsTableInfoModel.some((col: TableColumn) => col.name === 'lastUsedModel'); if (!hasLastUsedModelColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN lastUsedModel TEXT DEFAULT 'claude-sonnet-4-20250514'").run(); @@ -695,8 +711,8 @@ export class DatabaseService { // Add base_commit and base_branch columns to sessions table if they don't exist const sessionsTableInfoBase = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasBaseCommitColumn = sessionsTableInfoBase.some((col: any) => col.name === 'base_commit'); - const hasBaseBranchColumn = sessionsTableInfoBase.some((col: any) => col.name === 'base_branch'); + const hasBaseCommitColumn = sessionsTableInfoBase.some((col: TableColumn) => col.name === 'base_commit'); + const hasBaseBranchColumn = sessionsTableInfoBase.some((col: TableColumn) => col.name === 'base_branch'); if (!hasBaseCommitColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN base_commit TEXT").run(); @@ -710,9 +726,9 @@ export class DatabaseService { // Add commit mode settings columns to projects table if they don't exist const projectsTableInfoCommit = this.db.prepare("PRAGMA table_info(projects)").all(); - const hasCommitModeColumn = projectsTableInfoCommit.some((col: any) => col.name === 'commit_mode'); - const hasCommitStructuredPromptTemplateColumn = projectsTableInfoCommit.some((col: any) => col.name === 'commit_structured_prompt_template'); - const hasCommitCheckpointPrefixColumn = projectsTableInfoCommit.some((col: any) => col.name === 'commit_checkpoint_prefix'); + const hasCommitModeColumn = projectsTableInfoCommit.some((col: TableColumn) => col.name === 'commit_mode'); + const hasCommitStructuredPromptTemplateColumn = projectsTableInfoCommit.some((col: TableColumn) => col.name === 'commit_structured_prompt_template'); + const hasCommitCheckpointPrefixColumn = projectsTableInfoCommit.some((col: TableColumn) => col.name === 'commit_checkpoint_prefix'); if (!hasCommitModeColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN commit_mode TEXT DEFAULT 'checkpoint'").run(); @@ -731,8 +747,8 @@ export class DatabaseService { // Add commit mode settings columns to sessions table if they don't exist const sessionsTableInfoCommit = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasSessionCommitModeColumn = sessionsTableInfoCommit.some((col: any) => col.name === 'commit_mode'); - const hasSessionCommitModeSettingsColumn = sessionsTableInfoCommit.some((col: any) => col.name === 'commit_mode_settings'); + const hasSessionCommitModeColumn = sessionsTableInfoCommit.some((col: TableColumn) => col.name === 'commit_mode'); + const hasSessionCommitModeSettingsColumn = sessionsTableInfoCommit.some((col: TableColumn) => col.name === 'commit_mode_settings'); if (!hasSessionCommitModeColumn) { try { @@ -822,7 +838,7 @@ export class DatabaseService { updateProject(id: number, updates: Partial>): Project | undefined { const fields: string[] = []; - const values: any[] = []; + const values: (string | number | boolean | null)[] = []; if (updates.name !== undefined) { fields.push('name = ?'); @@ -986,7 +1002,7 @@ export class DatabaseService { updateFolder(id: string, updates: { name?: string; display_order?: number; parent_folder_id?: string | null }): void { const fields: string[] = []; - const values: any[] = []; + const values: (string | number | boolean | null)[] = []; if (updates.name !== undefined) { fields.push('name = ?'); @@ -1120,7 +1136,7 @@ export class DatabaseService { updateRunCommand(id: number, updates: { command?: string; display_name?: string; order_index?: number }): ProjectRunCommand | undefined { const fields: string[] = []; - const values: any[] = []; + const values: (string | number | boolean | null)[] = []; if (updates.command !== undefined) { fields.push('command = ?'); @@ -1221,7 +1237,7 @@ export class DatabaseService { console.log(`[Database] Updating session ${id} with data:`, data); const updates: string[] = []; - const values: any[] = []; + const values: (string | number | boolean | null)[] = []; if (data.name !== undefined) { updates.push('name = ?'); @@ -1532,26 +1548,28 @@ export class DatabaseService { SELECT MAX(execution_sequence) as max_seq FROM execution_diffs WHERE session_id = ? - `).get(sessionId) as any; + `).get(sessionId) as { max_seq: number | null } | undefined; return (result?.max_seq || 0) + 1; } - private convertDbExecutionDiff(row: any): ExecutionDiff { + private convertDbExecutionDiff(row: unknown): ExecutionDiff { + // Type assertion for database row + const dbRow = row as Record; return { - id: row.id, - session_id: row.session_id, - prompt_marker_id: row.prompt_marker_id, - execution_sequence: row.execution_sequence, - git_diff: row.git_diff, - files_changed: row.files_changed ? JSON.parse(row.files_changed) : [], - stats_additions: row.stats_additions, - stats_deletions: row.stats_deletions, - stats_files_changed: row.stats_files_changed, - before_commit_hash: row.before_commit_hash, - after_commit_hash: row.after_commit_hash, - commit_message: row.commit_message, - timestamp: row.timestamp + id: dbRow.id as number, + session_id: dbRow.session_id as string, + prompt_marker_id: dbRow.prompt_marker_id as number | null, + execution_sequence: dbRow.execution_sequence as number, + git_diff: dbRow.git_diff as string | null, + files_changed: dbRow.files_changed ? JSON.parse(dbRow.files_changed as string) : [], + stats_additions: dbRow.stats_additions as number, + stats_deletions: dbRow.stats_deletions as number, + stats_files_changed: dbRow.stats_files_changed as number, + before_commit_hash: dbRow.before_commit_hash as string | null, + after_commit_hash: dbRow.after_commit_hash as string | null, + commit_message: dbRow.commit_message as string | null, + timestamp: dbRow.timestamp as string }; } @@ -1611,7 +1629,7 @@ export class DatabaseService { name: string; type: string; notnull: number; - dflt_value: any; + dflt_value: string | null; pk: number }>; foreignKeys: Array<{ @@ -1638,7 +1656,7 @@ export class DatabaseService { name: string; type: string; notnull: number; - dflt_value: any; + dflt_value: string | null; pk: number; }>; From 9da1fd099adb6529e020ae6b23162b9de0160384 Mon Sep 17 00:00:00 2001 From: Jordan Bentley Date: Tue, 12 Aug 2025 11:50:01 -0400 Subject: [PATCH 13/14] Revert "checkpoint: https://github.com/stravu/crystal/pull/145" This reverts commit 16eb0fea4691c1963af74e7a943b0d2f4a458f8b. --- frontend/src/hooks/useSessionView.ts | 2 +- frontend/src/types/electron.d.ts | 86 +++++++--------- main/src/autoUpdater.ts | 7 +- main/src/database/database.ts | 140 ++++++++++++--------------- 4 files changed, 97 insertions(+), 138 deletions(-) diff --git a/frontend/src/hooks/useSessionView.ts b/frontend/src/hooks/useSessionView.ts index 56b03543..10dc2b6d 100644 --- a/frontend/src/hooks/useSessionView.ts +++ b/frontend/src/hooks/useSessionView.ts @@ -1471,7 +1471,7 @@ export const useSessionView = ( try { await API.sessions.rename(activeSession.id, editName.trim()); setIsEditingName(false); - } catch { + } catch (error) { alert('Failed to rename session'); setEditName(activeSession.name); setIsEditingName(false); diff --git a/frontend/src/types/electron.d.ts b/frontend/src/types/electron.d.ts index 7d19fcc3..2b1ea44d 100644 --- a/frontend/src/types/electron.d.ts +++ b/frontend/src/types/electron.d.ts @@ -1,10 +1,6 @@ // Type definitions for Electron preload API -import type { Session, CreateSessionRequest, SessionOutput, GitStatus } from './session'; -import type { Project, CreateProjectRequest, UpdateProjectRequest } from './project'; -import type { Folder } from './folder'; - -interface IPCResponse { +interface IPCResponse { success: boolean; data?: T; error?: string; @@ -14,7 +10,7 @@ interface IPCResponse { interface ElectronAPI { // Generic invoke method for direct IPC calls - invoke: (channel: string, ...args: unknown[]) => Promise; + invoke: (channel: string, ...args: any[]) => Promise; // Basic app info getAppVersion: () => Promise; @@ -41,7 +37,7 @@ interface ElectronAPI { getAllWithProjects: () => Promise; getArchivedWithProjects: () => Promise; get: (sessionId: string) => Promise; - create: (request: CreateSessionRequest) => Promise>; + create: (request: any) => Promise; delete: (sessionId: string) => Promise; sendInput: (sessionId: string, input: string) => Promise; continue: (sessionId: string, prompt?: string, model?: string) => Promise; @@ -113,9 +109,9 @@ interface ElectronAPI { projects: { getAll: () => Promise; getActive: () => Promise; - create: (projectData: CreateProjectRequest) => Promise>; + create: (projectData: any) => Promise; activate: (projectId: string) => Promise; - update: (projectId: string, updates: UpdateProjectRequest) => Promise>; + update: (projectId: string, updates: any) => Promise; delete: (projectId: string) => Promise; detectBranch: (path: string) => Promise; reorder: (projectOrders: Array<{ id: number; displayOrder: number }>) => Promise; @@ -142,7 +138,7 @@ interface ElectronAPI { // Configuration config: { get: () => Promise; - update: (updates: Record) => Promise; + update: (updates: any) => Promise; }; // Prompts @@ -159,13 +155,13 @@ interface ElectronAPI { // Dialog dialog: { - openFile: (options?: Electron.OpenDialogOptions) => Promise>; - openDirectory: (options?: Electron.OpenDialogOptions) => Promise>; + openFile: (options?: any) => Promise>; + openDirectory: (options?: any) => Promise>; }; // Permissions permissions: { - respond: (requestId: string, response: { approved: boolean; data?: unknown }) => Promise; + respond: (requestId: string, response: any) => Promise; getPending: () => Promise; }; @@ -184,8 +180,8 @@ interface ElectronAPI { dashboard: { getProjectStatus: (projectId: number) => Promise; getProjectStatusProgressive: (projectId: number) => Promise; - onUpdate: (callback: (data: unknown) => void) => () => void; - onSessionUpdate: (callback: (data: { sessionId: string; status?: string; progress?: number }) => void) => () => void; + onUpdate: (callback: (data: any) => void) => () => void; + onSessionUpdate: (callback: (data: any) => void) => () => void; }; // UI State management @@ -198,36 +194,36 @@ interface ElectronAPI { // Event listeners for real-time updates events: { - onSessionCreated: (callback: (session: Session) => void) => () => void; - onSessionUpdated: (callback: (session: Session) => void) => () => void; - onSessionDeleted: (callback: (session: Session) => void) => () => void; - onSessionsLoaded: (callback: (sessions: Session[]) => void) => () => void; - onSessionOutput: (callback: (output: SessionOutput) => void) => () => void; - onSessionOutputAvailable: (callback: (info: { sessionId: string; available: boolean }) => void) => () => void; - onGitStatusUpdated: (callback: (data: { sessionId: string; gitStatus: GitStatus }) => void) => () => void; + onSessionCreated: (callback: (session: any) => void) => () => void; + onSessionUpdated: (callback: (session: any) => void) => () => void; + onSessionDeleted: (callback: (session: any) => void) => () => void; + onSessionsLoaded: (callback: (sessions: any[]) => void) => () => void; + onSessionOutput: (callback: (output: any) => void) => () => void; + onSessionOutputAvailable: (callback: (info: any) => void) => () => void; + onGitStatusUpdated: (callback: (data: { sessionId: string; gitStatus: any }) => void) => () => void; onGitStatusLoading: (callback: (data: { sessionId: string }) => void) => () => void; onGitStatusLoadingBatch?: (callback: (sessionIds: string[]) => void) => () => void; - onGitStatusUpdatedBatch?: (callback: (updates: Array<{ sessionId: string; status: GitStatus }>) => void) => () => void; + onGitStatusUpdatedBatch?: (callback: (updates: Array<{ sessionId: string; status: any }>) => void) => () => void; // Project events - onProjectUpdated: (callback: (project: Project) => void) => () => void; + onProjectUpdated: (callback: (project: any) => void) => () => void; // Folder events - onFolderCreated: (callback: (folder: Folder) => void) => () => void; - onFolderUpdated: (callback: (folder: Folder) => void) => () => void; + onFolderCreated: (callback: (folder: any) => void) => () => void; + onFolderUpdated: (callback: (folder: any) => void) => () => void; onFolderDeleted: (callback: (folderId: string) => void) => () => void; - onScriptOutput: (callback: (output: { type: string; data: string }) => void) => () => void; + onScriptOutput: (callback: (output: any) => void) => () => void; onMainLog: (callback: (level: string, message: string) => void) => () => void; - onVersionUpdateAvailable: (callback: (versionInfo: { current: string; latest: string; updateAvailable: boolean }) => void) => () => void; + onVersionUpdateAvailable: (callback: (versionInfo: any) => void) => () => void; // Auto-updater events onUpdaterCheckingForUpdate: (callback: () => void) => () => void; - onUpdaterUpdateAvailable: (callback: (info: { version: string; releaseNotes?: string }) => void) => () => void; - onUpdaterUpdateNotAvailable: (callback: (info: { version: string }) => void) => () => void; - onUpdaterDownloadProgress: (callback: (progressInfo: { percent: number; bytesPerSecond: number; total?: number; transferred?: number }) => void) => () => void; - onUpdaterUpdateDownloaded: (callback: (info: { version: string; releaseNotes?: string }) => void) => () => void; - onUpdaterError: (callback: (error: { message: string; stack?: string }) => void) => () => void; + onUpdaterUpdateAvailable: (callback: (info: any) => void) => () => void; + onUpdaterUpdateNotAvailable: (callback: (info: any) => void) => () => void; + onUpdaterDownloadProgress: (callback: (progressInfo: any) => void) => () => void; + onUpdaterUpdateDownloaded: (callback: (info: any) => void) => () => void; + onUpdaterError: (callback: (error: any) => void) => () => void; // Process management events onZombieProcessesDetected: (callback: (data: { sessionId?: string | null; pids?: number[]; message: string }) => void) => () => void; @@ -243,7 +239,7 @@ interface ElectronAPI { name: string; type: string; notnull: number; - dflt_value: string | number | null; + dflt_value: any; pk: number; }>; foreignKeys: Array<{ @@ -265,28 +261,12 @@ interface ElectronAPI { }; } -// Define Electron dialog options if not already available -interface ElectronOpenDialogOptions { - title?: string; - defaultPath?: string; - buttonLabel?: string; - filters?: Array<{ name: string; extensions: string[] }>; - properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles' | 'createDirectory' | 'promptToCreate' | 'noResolveAliases' | 'treatPackageAsDirectory'>; - message?: string; - securityScopedBookmarks?: boolean; -} - -// Use global Electron type if available, otherwise use our definition -type Electron = { - OpenDialogOptions: ElectronOpenDialogOptions; -} - // Additional electron interface for IPC event listeners interface ElectronInterface { openExternal: (url: string) => Promise; - invoke: (channel: string, ...args: unknown[]) => Promise; - on: (channel: string, callback: (...args: unknown[]) => void) => void; - off: (channel: string, callback: (...args: unknown[]) => void) => void; + invoke: (channel: string, ...args: any[]) => Promise; + on: (channel: string, callback: (...args: any[]) => void) => void; + off: (channel: string, callback: (...args: any[]) => void) => void; } declare global { diff --git a/main/src/autoUpdater.ts b/main/src/autoUpdater.ts index 4ec9e47c..23d106d9 100644 --- a/main/src/autoUpdater.ts +++ b/main/src/autoUpdater.ts @@ -10,11 +10,8 @@ export function setupAutoUpdater(getMainWindow: () => BrowserWindow | null): voi // TEST MODE: Use local server for testing if (process.env.TEST_UPDATES === 'true') { - // Dynamic import to avoid ESLint warning - void (async () => { - const { setupTestUpdater } = await import('./test-updater'); - setupTestUpdater(); - })(); + const { setupTestUpdater } = require('./test-updater'); + setupTestUpdater(); console.log('[AutoUpdater] Using test update server at:', process.env.UPDATE_SERVER_URL || 'http://localhost:8080'); } else { // Configure electron-updater for production diff --git a/main/src/database/database.ts b/main/src/database/database.ts index 78ae4059..629ecc6c 100644 --- a/main/src/database/database.ts +++ b/main/src/database/database.ts @@ -3,21 +3,6 @@ import { readFileSync, mkdirSync } from 'fs'; import { join, dirname, basename } from 'path'; import type { Project, ProjectRunCommand, Folder, Session, SessionOutput, CreateSessionData, UpdateSessionData, ConversationMessage, PromptMarker, ExecutionDiff, CreateExecutionDiffData } from './models'; -// Type for SQLite PRAGMA table_info result -interface TableColumn { - cid: number; - name: string; - type: string; - notnull: number; - dflt_value: string | null; - pk: number; -} - -// Type for SQLite master table query result -interface TableInfo { - name: string; -} - export class DatabaseService { private db: Database.Database; @@ -50,9 +35,9 @@ export class DatabaseService { private runMigrations(): void { // Check if archived column exists const tableInfo = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasArchivedColumn = tableInfo.some((col: TableColumn) => col.name === 'archived'); - const hasInitialPromptColumn = tableInfo.some((col: TableColumn) => col.name === 'initial_prompt'); - const hasLastViewedAtColumn = tableInfo.some((col: TableColumn) => col.name === 'last_viewed_at'); + const hasArchivedColumn = tableInfo.some((col: any) => col.name === 'archived'); + const hasInitialPromptColumn = tableInfo.some((col: any) => col.name === 'initial_prompt'); + const hasLastViewedAtColumn = tableInfo.some((col: any) => col.name === 'last_viewed_at'); if (!hasArchivedColumn) { // Run migration to add archived column @@ -62,7 +47,7 @@ export class DatabaseService { // Check if we need to rename prompt to initial_prompt if (!hasInitialPromptColumn) { - const hasPromptColumn = tableInfo.some((col: TableColumn) => col.name === 'prompt'); + const hasPromptColumn = tableInfo.some((col: any) => col.name === 'prompt'); if (hasPromptColumn) { this.db.prepare("ALTER TABLE sessions RENAME COLUMN prompt TO initial_prompt").run(); } @@ -104,8 +89,8 @@ export class DatabaseService { } else { // Check if the table has the correct column name const promptMarkersInfo = this.db.prepare("PRAGMA table_info(prompt_markers)").all(); - const hasOutputLineColumn = promptMarkersInfo.some((col: TableColumn) => col.name === 'output_line'); - const hasTerminalLineColumn = promptMarkersInfo.some((col: TableColumn) => col.name === 'terminal_line'); + const hasOutputLineColumn = promptMarkersInfo.some((col: any) => col.name === 'output_line'); + const hasTerminalLineColumn = promptMarkersInfo.some((col: any) => col.name === 'terminal_line'); if (hasTerminalLineColumn && !hasOutputLineColumn) { // Rename the column from terminal_line to output_line @@ -149,14 +134,14 @@ export class DatabaseService { // Add commit_message column to execution_diffs if it doesn't exist const executionDiffsTableInfo = this.db.prepare("PRAGMA table_info(execution_diffs)").all(); - const hasCommitMessageColumn = executionDiffsTableInfo.some((col: TableColumn) => col.name === 'commit_message'); + const hasCommitMessageColumn = executionDiffsTableInfo.some((col: any) => col.name === 'commit_message'); if (!hasCommitMessageColumn) { this.db.prepare("ALTER TABLE execution_diffs ADD COLUMN commit_message TEXT").run(); } // Check if claude_session_id column exists const sessionTableInfoClaude = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasClaudeSessionIdColumn = sessionTableInfoClaude.some((col: TableColumn) => col.name === 'claude_session_id'); + const hasClaudeSessionIdColumn = sessionTableInfoClaude.some((col: any) => col.name === 'claude_session_id'); if (!hasClaudeSessionIdColumn) { // Add claude_session_id column to store Claude's actual session ID @@ -164,7 +149,7 @@ export class DatabaseService { } // Check if permission_mode column exists - const hasPermissionModeColumn = sessionTableInfoClaude.some((col: TableColumn) => col.name === 'permission_mode'); + const hasPermissionModeColumn = sessionTableInfoClaude.some((col: any) => col.name === 'permission_mode'); if (!hasPermissionModeColumn) { // Add permission_mode column to sessions table @@ -190,7 +175,7 @@ export class DatabaseService { // Add project_id to sessions table const sessionsTableInfoProjects = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasProjectIdColumn = sessionsTableInfoProjects.some((col: TableColumn) => col.name === 'project_id'); + const hasProjectIdColumn = sessionsTableInfoProjects.some((col: any) => col.name === 'project_id'); if (!hasProjectIdColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE").run(); @@ -199,8 +184,7 @@ export class DatabaseService { // Import existing config as default project if it exists try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { configManager } = require('../services/configManager') as typeof import('../services/configManager'); + const configManager = require('../services/configManager').configManager; const gitRepoPath = configManager.getGitRepoPath(); if (gitRepoPath) { @@ -227,7 +211,7 @@ export class DatabaseService { // Add is_main_repo column to sessions table if it doesn't exist const sessionTableInfoForMainRepo = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasIsMainRepoColumn = sessionTableInfoForMainRepo.some((col: TableColumn) => col.name === 'is_main_repo'); + const hasIsMainRepoColumn = sessionTableInfoForMainRepo.some((col: any) => col.name === 'is_main_repo'); if (!hasIsMainRepoColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN is_main_repo BOOLEAN DEFAULT 0").run(); @@ -236,28 +220,28 @@ export class DatabaseService { // Add main_branch column to projects table if it doesn't exist const projectsTableInfo = this.db.prepare("PRAGMA table_info(projects)").all(); - const hasMainBranchColumn = projectsTableInfo.some((col: TableColumn) => col.name === 'main_branch'); + const hasMainBranchColumn = projectsTableInfo.some((col: any) => col.name === 'main_branch'); if (!hasMainBranchColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN main_branch TEXT").run(); } // Add build_script column to projects table if it doesn't exist - const hasBuildScriptColumn = projectsTableInfo.some((col: TableColumn) => col.name === 'build_script'); + const hasBuildScriptColumn = projectsTableInfo.some((col: any) => col.name === 'build_script'); if (!hasBuildScriptColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN build_script TEXT").run(); } // Add default_permission_mode column to projects table if it doesn't exist - const hasDefaultPermissionModeColumn = projectsTableInfo.some((col: TableColumn) => col.name === 'default_permission_mode'); + const hasDefaultPermissionModeColumn = projectsTableInfo.some((col: any) => col.name === 'default_permission_mode'); if (!hasDefaultPermissionModeColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN default_permission_mode TEXT DEFAULT 'ignore' CHECK(default_permission_mode IN ('approve', 'ignore'))").run(); } // Add open_ide_command column to projects table if it doesn't exist - const hasOpenIdeCommandColumn = projectsTableInfo.some((col: TableColumn) => col.name === 'open_ide_command'); + const hasOpenIdeCommandColumn = projectsTableInfo.some((col: any) => col.name === 'open_ide_command'); if (!hasOpenIdeCommandColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN open_ide_command TEXT").run(); @@ -294,8 +278,8 @@ export class DatabaseService { // Check if display_order columns exist const projectsTableInfo2 = this.db.prepare("PRAGMA table_info(projects)").all(); const sessionsTableInfo2 = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasProjectsDisplayOrder = projectsTableInfo2.some((col: TableColumn) => col.name === 'display_order'); - const hasSessionsDisplayOrder = sessionsTableInfo2.some((col: TableColumn) => col.name === 'display_order'); + const hasProjectsDisplayOrder = projectsTableInfo2.some((col: any) => col.name === 'display_order'); + const hasSessionsDisplayOrder = sessionsTableInfo2.some((col: any) => col.name === 'display_order'); if (!hasProjectsDisplayOrder) { // Add display_order to projects @@ -337,18 +321,18 @@ export class DatabaseService { // Normalize timestamp fields migration // Check if last_viewed_at is still TEXT type const sessionTableInfoTimestamp = this.db.prepare("PRAGMA table_info(sessions)").all(); - const lastViewedAtColumn = sessionTableInfoTimestamp.find((col: TableColumn) => col.name === 'last_viewed_at') as TableColumn | undefined; + const lastViewedAtColumn = sessionTableInfoTimestamp.find((col: any) => col.name === 'last_viewed_at') as any; // Skip this migration if last_viewed_at_new already exists (migration partially completed) - const hasLastViewedAtNew = sessionTableInfoTimestamp.some((col: TableColumn) => col.name === 'last_viewed_at_new'); + const hasLastViewedAtNew = sessionTableInfoTimestamp.some((col: any) => col.name === 'last_viewed_at_new'); if (lastViewedAtColumn && lastViewedAtColumn.type === 'TEXT' && !hasLastViewedAtNew) { console.log('[Database] Running timestamp normalization migration...'); try { // Check if the new columns already exist (from a previous failed migration) - const hasLastViewedAtNew = sessionTableInfoTimestamp.some((col: TableColumn) => col.name === 'last_viewed_at_new'); - const hasRunStartedAtNew = sessionTableInfoTimestamp.some((col: TableColumn) => col.name === 'run_started_at_new'); + const hasLastViewedAtNew = sessionTableInfoTimestamp.some((col: any) => col.name === 'last_viewed_at_new'); + const hasRunStartedAtNew = sessionTableInfoTimestamp.some((col: any) => col.name === 'run_started_at_new'); // Create new temporary columns with DATETIME type if they don't exist if (!hasLastViewedAtNew) { @@ -416,7 +400,7 @@ export class DatabaseService { // Add missing completion_timestamp to prompt_markers if it doesn't exist const promptMarkersInfo = this.db.prepare("PRAGMA table_info(prompt_markers)").all(); - const hasCompletionTimestamp = promptMarkersInfo.some((col: TableColumn) => col.name === 'completion_timestamp'); + const hasCompletionTimestamp = promptMarkersInfo.some((col: any) => col.name === 'completion_timestamp'); if (!hasCompletionTimestamp) { this.db.prepare("ALTER TABLE prompt_markers ADD COLUMN completion_timestamp DATETIME").run(); @@ -424,7 +408,7 @@ export class DatabaseService { // Add is_favorite column to sessions table if it doesn't exist const sessionTableInfoFavorite = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasIsFavoriteColumn = sessionTableInfoFavorite.some((col: TableColumn) => col.name === 'is_favorite'); + const hasIsFavoriteColumn = sessionTableInfoFavorite.some((col: any) => col.name === 'is_favorite'); if (!hasIsFavoriteColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN is_favorite BOOLEAN DEFAULT 0").run(); @@ -432,7 +416,7 @@ export class DatabaseService { } // Add auto_commit column to sessions table if it doesn't exist - const hasAutoCommitColumn = sessionTableInfoFavorite.some((col: TableColumn) => col.name === 'auto_commit'); + const hasAutoCommitColumn = sessionTableInfoFavorite.some((col: any) => col.name === 'auto_commit'); if (!hasAutoCommitColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN auto_commit BOOLEAN DEFAULT 1").run(); @@ -440,7 +424,7 @@ export class DatabaseService { } // Add skip_continue_next column to sessions table if it doesn't exist - const hasSkipContinueNextColumn = sessionTableInfoFavorite.some((col: TableColumn) => col.name === 'skip_continue_next'); + const hasSkipContinueNextColumn = sessionTableInfoFavorite.some((col: any) => col.name === 'skip_continue_next'); if (!hasSkipContinueNextColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN skip_continue_next BOOLEAN DEFAULT 0").run(); @@ -458,7 +442,7 @@ export class DatabaseService { // Check if the old folders table has INTEGER id if (foldersExists) { const foldersInfo = this.db.prepare("PRAGMA table_info(folders)").all(); - const idColumn = foldersInfo.find((col: TableColumn) => col.name === 'id') as TableColumn | undefined; + const idColumn = foldersInfo.find((col: any) => col.name === 'id') as any; if (idColumn && idColumn.type === 'INTEGER') { // Old folders table with INTEGER id exists, drop it @@ -505,7 +489,7 @@ export class DatabaseService { // Update sessions table folder_id column type if needed const sessionTableInfo = this.db.prepare("PRAGMA table_info(sessions)").all(); - const folderIdColumn = sessionTableInfo.find((col: TableColumn) => col.name === 'folder_id') as TableColumn | undefined; + const folderIdColumn = sessionTableInfo.find((col: any) => col.name === 'folder_id') as any; if (folderIdColumn && folderIdColumn.type === 'INTEGER') { console.log('[Database] Converting sessions.folder_id from INTEGER to TEXT...'); @@ -585,7 +569,7 @@ export class DatabaseService { `).run(); // Add folder_id column to sessions table if it doesn't exist - const hasFolderIdColumn = sessionTableInfoFavorite.some((col: TableColumn) => col.name === 'folder_id'); + const hasFolderIdColumn = sessionTableInfoFavorite.some((col: any) => col.name === 'folder_id'); if (!hasFolderIdColumn) { this.db.prepare('ALTER TABLE sessions ADD COLUMN folder_id TEXT REFERENCES folders(id) ON DELETE SET NULL').run(); @@ -600,7 +584,7 @@ export class DatabaseService { // Add parent_folder_id column to folders table for nested folders support const foldersTableInfo = this.db.prepare("PRAGMA table_info(folders)").all(); - const hasParentFolderIdColumn = foldersTableInfo.some((col: TableColumn) => col.name === 'parent_folder_id'); + const hasParentFolderIdColumn = foldersTableInfo.some((col: any) => col.name === 'parent_folder_id'); if (!hasParentFolderIdColumn) { this.db.prepare('ALTER TABLE folders ADD COLUMN parent_folder_id TEXT REFERENCES folders(id) ON DELETE CASCADE').run(); @@ -645,7 +629,7 @@ export class DatabaseService { // Add model column to sessions table if it doesn't exist const sessionTableInfoModel = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasModelColumn = sessionTableInfoModel.some((col: TableColumn) => col.name === 'model'); + const hasModelColumn = sessionTableInfoModel.some((col: any) => col.name === 'model'); if (!hasModelColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN model TEXT DEFAULT 'claude-sonnet-4-20250514'").run(); @@ -693,7 +677,7 @@ export class DatabaseService { // Add worktree_folder column to projects table if it doesn't exist const projectsTableInfoWorktree = this.db.prepare("PRAGMA table_info(projects)").all(); - const hasWorktreeFolderColumn = projectsTableInfoWorktree.some((col: TableColumn) => col.name === 'worktree_folder'); + const hasWorktreeFolderColumn = projectsTableInfoWorktree.some((col: any) => col.name === 'worktree_folder'); if (!hasWorktreeFolderColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN worktree_folder TEXT").run(); @@ -702,7 +686,7 @@ export class DatabaseService { // Add lastUsedModel column to projects table if it doesn't exist const projectsTableInfoModel = this.db.prepare("PRAGMA table_info(projects)").all(); - const hasLastUsedModelColumn = projectsTableInfoModel.some((col: TableColumn) => col.name === 'lastUsedModel'); + const hasLastUsedModelColumn = projectsTableInfoModel.some((col: any) => col.name === 'lastUsedModel'); if (!hasLastUsedModelColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN lastUsedModel TEXT DEFAULT 'claude-sonnet-4-20250514'").run(); @@ -711,8 +695,8 @@ export class DatabaseService { // Add base_commit and base_branch columns to sessions table if they don't exist const sessionsTableInfoBase = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasBaseCommitColumn = sessionsTableInfoBase.some((col: TableColumn) => col.name === 'base_commit'); - const hasBaseBranchColumn = sessionsTableInfoBase.some((col: TableColumn) => col.name === 'base_branch'); + const hasBaseCommitColumn = sessionsTableInfoBase.some((col: any) => col.name === 'base_commit'); + const hasBaseBranchColumn = sessionsTableInfoBase.some((col: any) => col.name === 'base_branch'); if (!hasBaseCommitColumn) { this.db.prepare("ALTER TABLE sessions ADD COLUMN base_commit TEXT").run(); @@ -726,9 +710,9 @@ export class DatabaseService { // Add commit mode settings columns to projects table if they don't exist const projectsTableInfoCommit = this.db.prepare("PRAGMA table_info(projects)").all(); - const hasCommitModeColumn = projectsTableInfoCommit.some((col: TableColumn) => col.name === 'commit_mode'); - const hasCommitStructuredPromptTemplateColumn = projectsTableInfoCommit.some((col: TableColumn) => col.name === 'commit_structured_prompt_template'); - const hasCommitCheckpointPrefixColumn = projectsTableInfoCommit.some((col: TableColumn) => col.name === 'commit_checkpoint_prefix'); + const hasCommitModeColumn = projectsTableInfoCommit.some((col: any) => col.name === 'commit_mode'); + const hasCommitStructuredPromptTemplateColumn = projectsTableInfoCommit.some((col: any) => col.name === 'commit_structured_prompt_template'); + const hasCommitCheckpointPrefixColumn = projectsTableInfoCommit.some((col: any) => col.name === 'commit_checkpoint_prefix'); if (!hasCommitModeColumn) { this.db.prepare("ALTER TABLE projects ADD COLUMN commit_mode TEXT DEFAULT 'checkpoint'").run(); @@ -747,8 +731,8 @@ export class DatabaseService { // Add commit mode settings columns to sessions table if they don't exist const sessionsTableInfoCommit = this.db.prepare("PRAGMA table_info(sessions)").all(); - const hasSessionCommitModeColumn = sessionsTableInfoCommit.some((col: TableColumn) => col.name === 'commit_mode'); - const hasSessionCommitModeSettingsColumn = sessionsTableInfoCommit.some((col: TableColumn) => col.name === 'commit_mode_settings'); + const hasSessionCommitModeColumn = sessionsTableInfoCommit.some((col: any) => col.name === 'commit_mode'); + const hasSessionCommitModeSettingsColumn = sessionsTableInfoCommit.some((col: any) => col.name === 'commit_mode_settings'); if (!hasSessionCommitModeColumn) { try { @@ -838,7 +822,7 @@ export class DatabaseService { updateProject(id: number, updates: Partial>): Project | undefined { const fields: string[] = []; - const values: (string | number | boolean | null)[] = []; + const values: any[] = []; if (updates.name !== undefined) { fields.push('name = ?'); @@ -1002,7 +986,7 @@ export class DatabaseService { updateFolder(id: string, updates: { name?: string; display_order?: number; parent_folder_id?: string | null }): void { const fields: string[] = []; - const values: (string | number | boolean | null)[] = []; + const values: any[] = []; if (updates.name !== undefined) { fields.push('name = ?'); @@ -1136,7 +1120,7 @@ export class DatabaseService { updateRunCommand(id: number, updates: { command?: string; display_name?: string; order_index?: number }): ProjectRunCommand | undefined { const fields: string[] = []; - const values: (string | number | boolean | null)[] = []; + const values: any[] = []; if (updates.command !== undefined) { fields.push('command = ?'); @@ -1237,7 +1221,7 @@ export class DatabaseService { console.log(`[Database] Updating session ${id} with data:`, data); const updates: string[] = []; - const values: (string | number | boolean | null)[] = []; + const values: any[] = []; if (data.name !== undefined) { updates.push('name = ?'); @@ -1548,28 +1532,26 @@ export class DatabaseService { SELECT MAX(execution_sequence) as max_seq FROM execution_diffs WHERE session_id = ? - `).get(sessionId) as { max_seq: number | null } | undefined; + `).get(sessionId) as any; return (result?.max_seq || 0) + 1; } - private convertDbExecutionDiff(row: unknown): ExecutionDiff { - // Type assertion for database row - const dbRow = row as Record; + private convertDbExecutionDiff(row: any): ExecutionDiff { return { - id: dbRow.id as number, - session_id: dbRow.session_id as string, - prompt_marker_id: dbRow.prompt_marker_id as number | null, - execution_sequence: dbRow.execution_sequence as number, - git_diff: dbRow.git_diff as string | null, - files_changed: dbRow.files_changed ? JSON.parse(dbRow.files_changed as string) : [], - stats_additions: dbRow.stats_additions as number, - stats_deletions: dbRow.stats_deletions as number, - stats_files_changed: dbRow.stats_files_changed as number, - before_commit_hash: dbRow.before_commit_hash as string | null, - after_commit_hash: dbRow.after_commit_hash as string | null, - commit_message: dbRow.commit_message as string | null, - timestamp: dbRow.timestamp as string + id: row.id, + session_id: row.session_id, + prompt_marker_id: row.prompt_marker_id, + execution_sequence: row.execution_sequence, + git_diff: row.git_diff, + files_changed: row.files_changed ? JSON.parse(row.files_changed) : [], + stats_additions: row.stats_additions, + stats_deletions: row.stats_deletions, + stats_files_changed: row.stats_files_changed, + before_commit_hash: row.before_commit_hash, + after_commit_hash: row.after_commit_hash, + commit_message: row.commit_message, + timestamp: row.timestamp }; } @@ -1629,7 +1611,7 @@ export class DatabaseService { name: string; type: string; notnull: number; - dflt_value: string | null; + dflt_value: any; pk: number }>; foreignKeys: Array<{ @@ -1656,7 +1638,7 @@ export class DatabaseService { name: string; type: string; notnull: number; - dflt_value: string | null; + dflt_value: any; pk: number; }>; From 99e7fc0ac5be1cd29aa2f5c63e035019a7abfc08 Mon Sep 17 00:00:00 2001 From: Jordan Bentley Date: Tue, 12 Aug 2025 11:52:21 -0400 Subject: [PATCH 14/14] Remove accidentally added RichOutputView-upstream.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This file was accidentally added in PR #145. It appears to be a backup or merge artifact that was committed by mistake. The actual component lives at frontend/src/components/session/RichOutputView.tsx. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- RichOutputView-upstream.tsx | 1267 ----------------------------------- 1 file changed, 1267 deletions(-) delete mode 100644 RichOutputView-upstream.tsx diff --git a/RichOutputView-upstream.tsx b/RichOutputView-upstream.tsx deleted file mode 100644 index 67ba0510..00000000 --- a/RichOutputView-upstream.tsx +++ /dev/null @@ -1,1267 +0,0 @@ -import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import { API } from '../../utils/api'; -import { MarkdownPreview } from '../MarkdownPreview'; -import { User, Bot, ChevronDown, ChevronRight, Eye, EyeOff, Settings2, Wrench, CheckCircle, XCircle, Clock, ArrowDown } from 'lucide-react'; -import { parseTimestamp, formatDistanceToNow } from '../../utils/timestampUtils'; -import { ThinkingPlaceholder, InlineWorkingIndicator } from './ThinkingPlaceholder'; - -// Agent-agnostic message types for flexibility -interface RawMessage { - id?: string; - type: 'user' | 'assistant' | 'system' | 'tool_use' | 'tool_result' | 'result'; - role?: 'user' | 'assistant' | 'system'; - content?: string | any; - message?: { content?: string | any; [key: string]: any }; - timestamp: string; - name?: string; - input?: any; - tool_use_id?: string; - [key: string]: any; -} - -// Structured message representation for rendering -interface ConversationMessage { - id: string; - role: 'user' | 'assistant' | 'system'; - timestamp: string; - segments: MessageSegment[]; - metadata?: { - agent?: string; - model?: string; - duration?: number; - tokens?: number; - cost?: number; - systemSubtype?: string; - sessionInfo?: any; - }; -} - -// Different types of content within a message -type MessageSegment = - | { type: 'text'; content: string } - | { type: 'tool_call'; tool: ToolCall } - | { type: 'system_info'; info: any } - | { type: 'thinking'; content: string }; - -interface ToolCall { - id: string; - name: string; - input?: any; - result?: ToolResult; - status: 'pending' | 'success' | 'error'; -} - - -interface ToolResult { - content: string; - isError?: boolean; -} - -interface RichOutputViewProps { - sessionId: string; - sessionStatus?: string; - settings?: RichOutputSettings; -} - -// Settings stored in localStorage for persistence -export interface RichOutputSettings { - showToolCalls: boolean; - compactMode: boolean; - collapseTools: boolean; - showThinking: boolean; - showSessionInit: boolean; -} - -const defaultSettings: RichOutputSettings = { - showToolCalls: true, - compactMode: false, - collapseTools: false, - showThinking: true, - showSessionInit: false, // Hide by default - it's developer info -}; - -export const RichOutputView = React.forwardRef<{ scrollToPrompt: (promptIndex: number) => void }, RichOutputViewProps>( - ({ sessionId, sessionStatus, settings: propsSettings }, ref) => { - const [messages, setMessages] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [collapsedMessages, setCollapsedMessages] = useState>(new Set()); - const [expandedTools, setExpandedTools] = useState>(new Set()); - const [showScrollButton, setShowScrollButton] = useState(false); - - // Use parent-controlled settings if provided, otherwise use default - const localSettings = useMemo(() => { - const saved = localStorage.getItem('richOutputSettings'); - return saved ? JSON.parse(saved) : defaultSettings; - }, []); - - const settings = propsSettings || localSettings; - - const messagesEndRef = useRef(null); - const scrollContainerRef = useRef(null); - const isLoadingRef = useRef(false); - const userMessageRefs = useRef>(new Map()); - const wasAtBottomRef = useRef(false); - - // Save local settings to localStorage when they change - useEffect(() => { - if (!propsSettings) { - localStorage.setItem('richOutputSettings', JSON.stringify(localSettings)); - } - }, [localSettings, propsSettings]); - - // Expose scroll method via ref - React.useImperativeHandle(ref, () => ({ - scrollToPrompt: (promptIndex: number) => { - const messageDiv = userMessageRefs.current.get(promptIndex); - if (messageDiv && scrollContainerRef.current) { - // Scroll to the message with some offset from top - messageDiv.scrollIntoView({ behavior: 'smooth', block: 'center' }); - - // Add a highlight effect - messageDiv.classList.add('highlight-prompt'); - setTimeout(() => { - messageDiv.classList.remove('highlight-prompt'); - }, 2000); - } - } - }), []); - - // Extract text content from various message formats - const extractTextContent = (msg: RawMessage): string => { - // Handle Claude format: message.content as array of content blocks - if (msg.message?.content && Array.isArray(msg.message.content)) { - return msg.message.content - .filter(block => block.type === 'text') - .map(block => block.text || '') - .join('\n') - .trim(); - } - - // Handle direct string content - if (typeof msg.message?.content === 'string') { - return msg.message.content.trim(); - } - - // Handle direct content field - if (typeof msg.content === 'string') { - return msg.content.trim(); - } - - // Handle Gemini/other formats (future-proofing) - if (msg.message?.parts && Array.isArray(msg.message.parts)) { - return msg.message.parts - .filter(part => part.text) - .map(part => part.text) - .join('\n') - .trim(); - } - - return ''; - }; - - // Detect agent/model from message structure - const detectAgent = (msg: RawMessage): string => { - if (msg.message?.model?.includes('claude')) return 'claude'; - if (msg.message?.model?.includes('gemini')) return 'gemini'; - if (msg.message?.model?.includes('gpt')) return 'gpt-4'; - - // Fallback based on message structure - if (msg.message?.content && Array.isArray(msg.message.content)) return 'claude'; - if (msg.message?.parts) return 'gemini'; - - return 'unknown'; - }; - - // Transform raw messages into structured conversation messages - const transformMessages = (rawMessages: RawMessage[]): ConversationMessage[] => { - const transformed: ConversationMessage[] = []; - - // First pass: Build tool result map - const toolResults = new Map(); - rawMessages.forEach(msg => { - if (msg.type === 'user' && msg.message?.content && Array.isArray(msg.message.content)) { - msg.message.content.forEach((block: any) => { - if (block.type === 'tool_result' && block.tool_use_id) { - toolResults.set(block.tool_use_id, { - content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content), - isError: block.is_error || false - }); - } - }); - } - }); - - // Second pass: Build conversation messages - for (let i = 0; i < rawMessages.length; i++) { - const msg = rawMessages[i]; - - if (msg.type === 'user') { - // Check if this is a tool result message - let hasToolResult = false; - let hasOnlyText = true; - - if (msg.message?.content && Array.isArray(msg.message.content)) { - hasToolResult = msg.message.content.some((block: any) => block.type === 'tool_result'); - hasOnlyText = msg.message.content.every((block: any) => block.type === 'text'); - } - - // Only show real user prompts (text-only messages without tool results) - if (!hasToolResult && hasOnlyText) { - const textContent = extractTextContent(msg); - - if (textContent) { - const userMessage = { - id: msg.id || `user-${i}-${msg.timestamp}`, - role: 'user' as const, - timestamp: msg.timestamp, - segments: [{ type: 'text', content: textContent } as MessageSegment], - metadata: { agent: detectAgent(msg) } - }; - - transformed.push(userMessage); - - // Removed: Commit summaries are now shown in the dedicated Commits panel - } - } - // Skip tool result messages - they're attached to assistant messages - - } else if (msg.type === 'assistant') { - const segments: MessageSegment[] = []; - - - // Check for direct text field first (some messages come this way) - if (msg.text && typeof msg.text === 'string') { - segments.push({ type: 'text', content: msg.text.trim() }); - } else if (msg.message?.content && Array.isArray(msg.message.content)) { - // Process each content block - msg.message.content.forEach((block: any) => { - if (block.type === 'text' && block.text?.trim()) { - segments.push({ type: 'text', content: block.text.trim() }); - } else if (block.type === 'thinking') { - const thinkingContent = block.thinking || block.content || block.text; - if (thinkingContent && typeof thinkingContent === 'string' && thinkingContent.trim()) { - segments.push({ type: 'thinking', content: thinkingContent.trim() }); - } - } else if (block.type === 'tool_use') { - const toolCall: ToolCall = { - id: block.id, - name: block.name, - input: block.input, - status: toolResults.has(block.id) ? 'success' : 'pending', - result: toolResults.get(block.id) - }; - segments.push({ type: 'tool_call', tool: toolCall }); - } - }); - } else { - // Fallback for other formats - const textContent = extractTextContent(msg); - if (textContent) { - segments.push({ type: 'text', content: textContent }); - } - } - - // Only add message if it has content - if (segments.length > 0) { - // Check if this is a synthetic error message - const isSyntheticError = msg.message?.model === '' && - segments.some(seg => seg.type === 'text' && - (seg.content.includes('Prompt is too long') || - seg.content.includes('API Error') || - seg.content.includes('error'))); - - transformed.push({ - id: msg.id || `assistant-${i}-${msg.timestamp}`, - role: isSyntheticError ? 'system' : 'assistant', - timestamp: msg.timestamp, - segments, - metadata: { - agent: detectAgent(msg), - model: msg.message?.model, - duration: msg.message?.duration, - tokens: msg.message?.usage ? - (msg.message.usage.input_tokens || 0) + (msg.message.usage.output_tokens || 0) : - undefined, - cost: msg.message?.usage?.cost, - systemSubtype: isSyntheticError ? 'error' : undefined - } - }); - } - - } else if (msg.type === 'system' && msg.subtype === 'init') { - // Include system init messages - transformed.push({ - id: msg.id || `system-init-${i}-${msg.timestamp}`, - role: 'system', - timestamp: msg.timestamp, - segments: [{ - type: 'system_info', - info: { - cwd: msg.cwd, - model: msg.model, - tools: msg.tools, - mcp_servers: msg.mcp_servers, - permissionMode: msg.permissionMode, - session_id: msg.session_id - } - }], - metadata: { - systemSubtype: 'init', - sessionInfo: msg - } - }); - - } else if (msg.type === 'system' && msg.subtype === 'context_compacted') { - // Handle context compaction messages - transformed.push({ - id: msg.id || `context-compacted-${i}-${msg.timestamp}`, - role: 'system', - timestamp: msg.timestamp, - segments: [{ - type: 'text', - content: msg.summary || '' - }, { - type: 'system_info', - info: { - message: msg.message - } - }], - metadata: { - systemSubtype: 'context_compacted' - } - }); - - } else if (msg.type === 'system' && msg.subtype === 'error') { - // Handle error messages from session manager - transformed.push({ - id: msg.id || `error-${i}-${msg.timestamp}`, - role: 'system', - timestamp: msg.timestamp, - segments: [{ - type: 'system_info', - info: { - error: msg.error, - details: msg.details, - message: msg.message - } - }], - metadata: { - systemSubtype: 'error' - } - }); - - } else if (msg.type === 'system' && msg.subtype === 'git_operation') { - // Handle git operation messages - transformed.push({ - id: msg.id || `git-operation-${i}-${msg.timestamp}`, - role: 'system', - timestamp: msg.timestamp, - segments: [{ - type: 'text', - content: msg.message || msg.raw_output || '' - }], - metadata: { - systemSubtype: 'git_operation' - } - }); - - } else if (msg.type === 'system' && msg.subtype === 'git_error') { - // Handle git error messages - transformed.push({ - id: msg.id || `git-error-${i}-${msg.timestamp}`, - role: 'system', - timestamp: msg.timestamp, - segments: [{ - type: 'text', - content: msg.message || msg.raw_output || '' - }], - metadata: { - systemSubtype: 'git_error' - } - }); - - } else if (msg.type === 'result') { - // Handle execution result messages - especially errors - if (msg.is_error && msg.result) { - transformed.push({ - id: msg.id || `error-${i}-${msg.timestamp}`, - role: 'system', - timestamp: msg.timestamp, - segments: [{ - type: 'text', - content: `Error: ${msg.result}` - }], - metadata: { - systemSubtype: 'error', - duration: msg.duration_ms, - cost: msg.total_cost_usd - } - }); - } - // Skip non-error result messages - continue; - } - } - - return transformed; - }; - - - const loadMessages = useCallback(async () => { - // Prevent concurrent loads using ref - if (isLoadingRef.current) return; - isLoadingRef.current = true; - - try { - setError(null); - - // Load conversation messages and JSON messages - const [conversationResponse, outputResponse] = await Promise.all([ - API.sessions.getConversation(sessionId), - API.sessions.getJsonMessages(sessionId) - ]); - - // Combine both sources - conversation messages have the actual user prompts - const userPrompts: RawMessage[] = []; - if (conversationResponse.success && Array.isArray(conversationResponse.data)) { - conversationResponse.data.forEach((msg: any) => { - if (msg.message_type === 'user') { - userPrompts.push({ - type: 'user', - message: { - role: 'user', - content: [{ type: 'text', text: msg.content }] - }, - timestamp: msg.timestamp - }); - } - }); - } - - // Combine user prompts with output messages (filter for JSON messages) - const allMessages = [...userPrompts]; - if (outputResponse.success && outputResponse.data && Array.isArray(outputResponse.data)) { - // JSON messages are already in the correct format from getJsonMessages - allMessages.push(...outputResponse.data); - } - - // Sort by timestamp to get correct order - allMessages.sort((a, b) => { - const timeA = new Date(a.timestamp).getTime(); - const timeB = new Date(b.timestamp).getTime(); - return timeA - timeB; - }); - - - const conversationMessages = transformMessages(allMessages); - setMessages(conversationMessages); - } catch (err) { - console.error('Failed to load messages:', err); - setError('Failed to load conversation history'); - } finally { - setLoading(false); - isLoadingRef.current = false; - } - }, [sessionId]); - - // Listen for real-time output updates - debounced to prevent performance issues - useEffect(() => { - let debounceTimer: NodeJS.Timeout; - - const handleOutputAvailable = (event: CustomEvent<{ sessionId: string }>) => { - if (event.detail.sessionId === sessionId) { - // Debounce message reloading to prevent excessive re-renders - clearTimeout(debounceTimer); - debounceTimer = setTimeout(() => { - loadMessages(); - }, 500); // Wait 500ms after last event - } - }; - - window.addEventListener('session-output-available', handleOutputAvailable as any); - - return () => { - clearTimeout(debounceTimer); - window.removeEventListener('session-output-available', handleOutputAvailable as any); - }; - }, [sessionId, loadMessages]); - - // Initial load - useEffect(() => { - if (!sessionId) return; - loadMessages(); - }, [sessionId, loadMessages]); - - // Track if user is at bottom - set up once when container is available - useEffect(() => { - let cleanup: (() => void) | undefined; - - // Use a small delay to ensure DOM is ready - const timer = setTimeout(() => { - const container = scrollContainerRef.current; - if (!container) return; - - const checkIfAtBottom = () => { - const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 50; - wasAtBottomRef.current = isAtBottom; - }; - - // Check initial position - checkIfAtBottom(); - - container.addEventListener('scroll', checkIfAtBottom); - - // Store cleanup function - cleanup = () => container.removeEventListener('scroll', checkIfAtBottom); - }, 100); - - return () => { - clearTimeout(timer); - if (cleanup) cleanup(); - }; - }, []); // Empty array - set up only once - - // Auto-scroll to bottom when messages change or view loads - useEffect(() => { - if (messagesEndRef.current && !loading) { - // Scroll if we were at the bottom before the update - if (wasAtBottomRef.current) { - // Use requestAnimationFrame to ensure DOM has updated - requestAnimationFrame(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' as ScrollBehavior }); - // Don't set wasAtBottomRef here - let the scroll event handler determine actual position - }); - } - } - }, [messages, loading]); - - // Handle scroll events to show/hide scroll button - useEffect(() => { - const container = scrollContainerRef.current; - if (!container) return; - - const handleScroll = () => { - const { scrollTop, scrollHeight, clientHeight } = container; - // Show button if scrolled up more than one viewport height - const scrolledUp = scrollHeight - scrollTop - clientHeight; - setShowScrollButton(scrolledUp > clientHeight); - }; - - container.addEventListener('scroll', handleScroll); - // Check initial state - handleScroll(); - - return () => container.removeEventListener('scroll', handleScroll); - }, [messages]); - - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }; - - const toggleMessageCollapse = (messageId: string) => { - setCollapsedMessages(prev => { - const next = new Set(prev); - if (next.has(messageId)) { - next.delete(messageId); - } else { - next.add(messageId); - } - return next; - }); - }; - - const toggleToolExpand = (toolId: string) => { - setExpandedTools(prev => { - const next = new Set(prev); - if (next.has(toolId)) { - next.delete(toolId); - } else { - next.add(toolId); - } - return next; - }); - }; - - - // Render a tool call segment - const renderToolCall = (tool: ToolCall) => { - const isExpanded = !settings.collapseTools || expandedTools.has(tool.id); - - return ( -
- - - {isExpanded && ( -
- {/* Tool Parameters */} - {tool.input && Object.keys(tool.input).length > 0 && ( -
-
Parameters:
- {formatToolInput(tool.name, tool.input)} -
- )} - - {/* Tool Result */} - {tool.result && ( -
-
- {tool.result.isError ? 'Error:' : 'Result:'} -
-
- {formatToolResult(tool.name, tool.result.content)} -
-
- )} - - {/* Pending state */} - {tool.status === 'pending' && ( -
Waiting for result...
- )} -
- )} -
- ); - }; - - // Format tool input for display - const formatToolInput = (toolName: string, input: any): React.ReactNode => { - switch (toolName) { - case 'Read': - return ( -
- {input.file_path && ( -
- File: - {input.file_path} -
- )} - {input.offset &&
Lines: {input.offset}-{input.offset + (input.limit || 2000)}
} -
- ); - - case 'Edit': - case 'MultiEdit': - return ( -
- {input.file_path &&
File: {input.file_path}
} - {toolName === 'MultiEdit' && input.edits && ( -
{input.edits.length} changes
- )} -
- ); - - case 'Write': - return ( -
- {input.file_path &&
File: {input.file_path}
} - {input.content && ( -
{input.content.split('\n').length} lines
- )} -
- ); - - case 'Bash': - return ( -
- $ {input.command} -
- ); - - case 'Grep': - return ( -
-
Pattern: "{input.pattern}"
- {input.path &&
Path: {input.path}
} - {input.glob &&
Files: {input.glob}
} -
- ); - - case 'TodoWrite': - return ( -
- {input.todos && input.todos.map((todo: any, idx: number) => { - const icon = todo.status === 'completed' ? '✓' : - todo.status === 'in_progress' ? '→' : '○'; - const color = todo.status === 'completed' ? 'text-status-success' : - todo.status === 'in_progress' ? 'text-status-warning' : 'text-text-tertiary'; - return ( -
- {icon} {todo.content} -
- ); - })} -
- ); - - default: - // Compact display for unknown tools - return ( -
-            {JSON.stringify(input, null, 2)}
-          
- ); - } - }; - - // Format tool result for display - const formatToolResult = (_toolName: string, result: string): React.ReactNode => { - if (!result) { - return
No result
; - } - - try { - // Check if result is JSON - const parsed = JSON.parse(result); - - // Handle image reads - if (Array.isArray(parsed) && parsed[0]?.type === 'image') { - return ( -
- [Image displayed to assistant] -
- ); - } - - // For other JSON results, pretty print compactly - return ( -
-          {JSON.stringify(parsed, null, 2)}
-        
- ); - } catch { - // Not JSON, display as text - if (result.length > 300) { - return ( -
- - {result.substring(0, 100)}... (click to expand) - -
{result}
-
- ); - } - - return
{result}
; - } - }; - - // Render a complete message - const renderMessage = (message: ConversationMessage, index: number, userMessageIndex?: number) => { - const isCollapsed = collapsedMessages.has(message.id); - const isUser = message.role === 'user'; - const isSystem = message.role === 'system'; - const hasTextContent = message.segments.some(seg => seg.type === 'text'); - const textContent = message.segments - .filter(seg => seg.type === 'text') - .map(seg => seg.type === 'text' ? seg.content : '') - .join('\n\n'); - - // Check if message has tool calls or thinking - const hasToolCalls = message.segments.some(seg => seg.type === 'tool_call'); - const hasThinking = message.segments.some(seg => seg.type === 'thinking'); - - // Determine if we need extra spacing before this message - const prevMessage = index > 0 ? filteredMessages[index - 1] : null; - const needsExtraSpacing = prevMessage && ( - (prevMessage.role !== message.role) || - (hasThinking && !prevMessage.segments.some(seg => seg.type === 'thinking')) - ); - - // Special rendering for system messages - if (isSystem) { - if (message.metadata?.systemSubtype === 'init') { - const info = message.segments.find(seg => seg.type === 'system_info'); - if (info?.type === 'system_info') { - return ( -
-
-
- -
-
-
- Session Started - - {formatDistanceToNow(parseTimestamp(message.timestamp))} - -
-
-
Model: {info.info.model}
-
Working Directory: {info.info.cwd}
-
- Tools: {info.info.tools?.length || 0} available -
-
-
-
-
- ); - } - } else if (message.metadata?.systemSubtype === 'error') { - // Render error messages - get error info from system_info segment - const errorInfo = message.segments.find(seg => seg.type === 'system_info')?.info; - const errorMessage = errorInfo?.message || textContent; - const errorTitle = errorInfo?.error || 'Session Error'; - - return ( -
-
-
- -
-
-
- {errorTitle} - - {formatDistanceToNow(parseTimestamp(message.timestamp))} - - {message.metadata?.duration && ( - - · {(message.metadata.duration / 1000).toFixed(1)}s - - )} -
-
- {errorMessage} -
-
-
-
- ); - } else if (message.metadata?.systemSubtype === 'context_compacted') { - // Render context compaction messages - // Get the message info from the system_info segment - const infoSegment = message.segments.find(seg => seg.type === 'system_info'); - const helpMessage = infoSegment?.type === 'system_info' ? infoSegment.info.message : - 'Context has been compacted. You can continue chatting - your next message will automatically include the context summary above.'; - - return ( -
-
-
- - - -
-
-
- Context Compacted - - {formatDistanceToNow(parseTimestamp(message.timestamp))} - -
- - {/* Summary content */} -
-
- {textContent} -
-
- - {/* Clear instruction message */} -
- -
- Ready to continue! {helpMessage} -
-
-
-
-
- ); - } else if (message.metadata?.systemSubtype === 'git_operation') { - // Render git operation messages - const rawOutput = message.segments.find(seg => seg.type === 'text')?.content || textContent; - const isSuccess = rawOutput.includes('✓') || rawOutput.includes('Successfully'); - - // Parse the git operation message for better formatting - const lines = rawOutput.split('\n'); - const mainMessage = lines.filter(line => !line.includes('🔄 GIT OPERATION') && line.trim()).join('\n'); - - return ( -
-
-
- - - -
-
-
- - 🔄 Git Operation - - - {formatDistanceToNow(parseTimestamp(message.timestamp))} - -
-
- {mainMessage.split('\n').map((line, idx) => { - const trimmedLine = line.trim(); - if (!trimmedLine) return null; - - if (trimmedLine.startsWith('✓')) { - return ( -
- - {trimmedLine.substring(1).trim()} -
- ); - } else if (trimmedLine.startsWith('Commit message:')) { - return ( -
- {trimmedLine} -
- ); - } else if (trimmedLine.includes('Git output:')) { - return ( -
- {trimmedLine} -
- ); - } else { - return ( -
- {trimmedLine} -
- ); - } - })} -
-
-
-
- ); - } else if (message.metadata?.systemSubtype === 'git_error') { - // Render git error messages - const rawOutput = message.segments.find(seg => seg.type === 'text')?.content || textContent; - - // Parse the git error message for better formatting - const lines = rawOutput.split('\n'); - const mainMessage = lines.filter(line => line.trim()).join('\n'); - - return ( -
-
-
- -
-
-
- Git Operation Failed - - {formatDistanceToNow(parseTimestamp(message.timestamp))} - -
-
- {mainMessage.split('\n').map((line, idx) => { - const trimmedLine = line.trim(); - if (!trimmedLine) return null; - - if (trimmedLine.startsWith('✗')) { - return ( -
- - {trimmedLine.substring(1).trim()} -
- ); - } else if (trimmedLine.includes('Git output:')) { - return ( -
- {trimmedLine} -
- ); - } else { - return ( -
- {trimmedLine} -
- ); - } - })} -
-
-
-
- ); - } - } - - return ( -
{ - if (el) userMessageRefs.current.set(userMessageIndex, el); - } : undefined} - className={` - rounded-lg transition-all - ${isUser ? 'bg-surface-secondary' : hasThinking ? 'bg-surface-primary/50' : 'bg-surface-primary'} - ${hasToolCalls ? 'bg-surface-tertiary/30' : ''} - ${settings.compactMode ? 'p-3' : 'p-4'} - ${needsExtraSpacing ? 'mt-4' : ''} - `} - > - {/* Message Header */} -
-
- {isUser ? : } -
-
- - {isUser ? 'You' : getAgentName(message.metadata?.agent)} - - - {formatDistanceToNow(parseTimestamp(message.timestamp))} - - {message.metadata?.duration && ( - - · {(message.metadata.duration / 1000).toFixed(1)}s - - )} -
- {hasTextContent && textContent.length > 200 && ( - - )} -
- - {/* Message Content */} -
- {/* Thinking segments */} - {settings.showThinking && message.segments - .filter(seg => seg.type === 'thinking') - .map((seg, idx) => { - if (seg.type === 'thinking') { - return ( -
-
-
-
- -
-
-
- ); - } - return null; - }) - } - - {/* Text segments */} - {hasTextContent && ( -
- {isUser ? ( -
{textContent}
- ) : ( -
- -
- )} - {isCollapsed && ( -
- )} -
- )} - - {/* Tool calls */} - {settings.showToolCalls && message.segments - .filter(seg => seg.type === 'tool_call') - .map((seg, idx) => { - if (seg.type === 'tool_call') { - return ( -
- {renderToolCall(seg.tool)} -
- ); - } - return null; - }) - } - -
-
- ); - }; - - const getAgentName = (agent?: string) => { - switch (agent) { - case 'claude': return 'Claude'; - case 'gpt-4': - case 'openai': return 'GPT-4'; - case 'gemini': - case 'google': return 'Gemini'; - default: return 'Assistant'; - } - }; - - // Check if we're waiting for Claude's response - const isWaitingForResponse = useMemo(() => { - // Always show placeholder if session is actively running - if (sessionStatus === 'running') { - return true; - } - - // Also show if waiting and last message is from user - if (sessionStatus === 'waiting' && messages.length > 0) { - const lastMessage = messages[messages.length - 1]; - return lastMessage.role === 'user'; - } - - return false; - }, [messages, sessionStatus]); - - // Filter messages based on settings - const filteredMessages = useMemo(() => { - if (settings.showSessionInit) { - return messages; - } - // Filter out session init messages - return messages.filter(msg => !(msg.role === 'system' && msg.metadata?.systemSubtype === 'init')); - }, [messages, settings.showSessionInit]); - - // Memoize the rendered messages to prevent unnecessary re-renders - const renderedMessages = useMemo(() => { - let userMessageIndex = 0; - return filteredMessages.map((msg, idx) => { - const isUser = msg.role === 'user'; - const element = renderMessage(msg, idx, isUser ? userMessageIndex : undefined); - if (isUser) userMessageIndex++; - return element; - }); - }, [filteredMessages, collapsedMessages, expandedTools, settings]); - - if (loading) { - return ( -
-
Loading conversation...
-
- ); - } - - if (error) { - return ( -
-
{error}
-
- ); - } - - return ( -
- {/* Settings panel is now rendered in SessionView to avoid duplication */} - - {/* Messages */} -
-
- {filteredMessages.length === 0 && !isWaitingForResponse ? ( -
- No messages to display -
- ) : ( -
- {renderedMessages} - {isWaitingForResponse && ( - filteredMessages.length === 0 || - (filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1].role === 'user') ? ( - - ) : ( - - ) - )} -
-
- )} -
- - {/* Scroll to bottom button - centered above input */} - {showScrollButton && ( -
- -
- )} -
-
- ); -}); - -RichOutputView.displayName = 'RichOutputView'; \ No newline at end of file