ompchamber 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +131 -147
  2. package/dist/client/index.html +4 -2
  3. package/dist/client/static/css/index.5f630421b7.css +2 -0
  4. package/dist/client/static/js/77.0d81c74cf0.js +139 -0
  5. package/dist/client/static/js/async/1k.973e8e21fc.js +2 -0
  6. package/dist/client/static/js/async/{3y.3186ecdd63.js → 3y.3530b824ad.js} +6 -6
  7. package/dist/client/static/js/async/{q.b453193538.js → 4m.928a1dde85.js} +10 -10
  8. package/dist/client/static/js/async/66.5dfa29f957.js +93 -0
  9. package/dist/client/static/js/async/7g.a7814350c3.js +1 -0
  10. package/dist/client/static/js/async/ar.9303e5f2e8.js +2 -0
  11. package/dist/client/static/js/async/{cx.9c8268d542.js → cx.3cb0242a3a.js} +1 -1
  12. package/dist/client/static/js/async/e.93214d622b.js +79 -0
  13. package/dist/client/static/js/async/g.74ff36419f.js +2 -0
  14. package/dist/client/static/js/async/{m.2e1ea3db9e.js.LICENSE.txt → g.74ff36419f.js.LICENSE.txt} +5 -0
  15. package/dist/client/static/js/async/h1.393f066d09.js +1 -0
  16. package/dist/client/static/js/async/jl.874d352278.js +1 -0
  17. package/dist/client/static/js/async/{k.d16cd09ae7.js → k.3210286401.js} +1 -1
  18. package/dist/client/static/js/async/{m.2e1ea3db9e.js → m.eb2e6752e2.js} +3 -3
  19. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +11 -0
  20. package/dist/client/static/js/async/mt.0352e976d8.js +1 -0
  21. package/dist/client/static/js/async/{oc.eb22093f05.js → oc.b73042e8bd.js} +1 -1
  22. package/dist/client/static/js/async/{og.53f068804c.js → og.8eb577893a.js} +1 -1
  23. package/dist/client/static/js/async/px.fc6877924b.js +1 -0
  24. package/dist/client/static/js/async/q.33d6779f98.js +21 -0
  25. package/dist/client/static/js/async/{q5.88b50ffa95.js → q5.97dc1481b6.js} +1 -1
  26. package/dist/client/static/js/async/s.0f395cdda5.js +94 -0
  27. package/dist/client/static/js/async/{tr.2a0d2b7600.js → tr.e878658140.js} +1 -1
  28. package/dist/client/static/js/async/vt.42416b707d.js +1 -0
  29. package/dist/client/static/js/async/x1.da8687159b.js +90 -0
  30. package/dist/client/static/js/async/xe.da8dcc2015.js +2 -0
  31. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +11 -0
  32. package/dist/client/static/js/async/{xn.6360379f71.js → xn.6510c0abe2.js} +2 -2
  33. package/dist/client/static/js/async/yg.35bebd6d0c.js +149 -0
  34. package/dist/client/static/js/async/yv.d99d829136.js +1 -0
  35. package/dist/client/static/js/index.c05535dbde.js +38 -0
  36. package/package.json +3 -3
  37. package/src/client/components/common/ComposerTextarea.tsx +9 -1
  38. package/src/client/components/common/MarkdownRenderer.tsx +50 -12
  39. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +9 -3
  40. package/src/client/components/settings/Modal.tsx +11 -2
  41. package/src/client/components/settings/categories/AppearanceSettings.tsx +7 -30
  42. package/src/client/components/settings/categories/ProviderSettings.tsx +2 -0
  43. package/src/client/components/settings/categories/appearance-settings/ThemeCard.tsx +61 -0
  44. package/src/client/components/settings/categories/appearance-settings/ThemeGrid.tsx +80 -0
  45. package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +1 -1
  46. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +43 -6
  47. package/src/client/components/settings/categories/provider-settings/ModelConfigModal.tsx +54 -54
  48. package/src/client/components/workspace/SearchPanel.tsx +16 -3
  49. package/src/client/components/workspace/btw-panel/AskCard.tsx +11 -6
  50. package/src/client/components/workspace/btw-panel/Messages.tsx +105 -51
  51. package/src/client/components/workspace/btw-panel/index.tsx +124 -45
  52. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +30 -14
  53. package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +10 -1
  54. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +8 -2
  55. package/src/client/components/workspace/chat-timeline/MessageItem.tsx +27 -16
  56. package/src/client/components/workspace/chat-timeline/MessageList.tsx +11 -0
  57. package/src/client/components/workspace/chat-timeline/RunFooter.tsx +8 -0
  58. package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +12 -1
  59. package/src/client/components/workspace/chat-timeline/index.tsx +7 -2
  60. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx +42 -15
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.tsx +13 -1
  62. package/src/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/index.tsx +13 -4
  63. package/src/client/components/workspace/editor/index.tsx +11 -2
  64. package/src/client/components/workspace/file-explorer/GitRepoDropdown.tsx +15 -41
  65. package/src/client/components/workspace/file-explorer/index.tsx +86 -16
  66. package/src/client/components/workspace/git-panel/index.tsx +60 -78
  67. package/src/client/components/workspace/terminal-panel/Header.tsx +16 -1
  68. package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +5 -5
  69. package/src/client/components/workspace/terminal-panel/index.tsx +6 -2
  70. package/src/client/components/workspace/terminal-panel/mount.ts +2 -2
  71. package/src/client/data/theme/terminal.ts +50 -52
  72. package/src/client/hooks/chat/btw/index.ts +139 -18
  73. package/src/client/hooks/chat/btw/intercept.ts +8 -8
  74. package/src/client/hooks/chat/btw/mode.ts +111 -16
  75. package/src/client/hooks/chat/composer/index.ts +10 -4
  76. package/src/client/hooks/chat/timeline/access-mode.ts +41 -0
  77. package/src/client/hooks/chat/timeline/ask-frames.test.ts +87 -0
  78. package/src/client/hooks/chat/timeline/ask-frames.ts +20 -1
  79. package/src/client/hooks/chat/timeline/auto-scroll.ts +13 -4
  80. package/src/client/hooks/chat/timeline/index.ts +8 -20
  81. package/src/client/hooks/chat/timeline/queue.ts +66 -9
  82. package/src/client/hooks/chat/timeline/scroll.test.ts +260 -0
  83. package/src/client/hooks/chat/timeline/scroll.ts +93 -20
  84. package/src/client/hooks/chat/timeline/session-load.ts +7 -1
  85. package/src/client/hooks/chat/timeline/session-pagination.test.ts +167 -0
  86. package/src/client/hooks/chat/timeline/session-pagination.ts +37 -11
  87. package/src/client/hooks/settings/providers.ts +34 -3
  88. package/src/client/hooks/ui/open-file.ts +7 -1
  89. package/src/client/hooks/ui/theme.ts +82 -57
  90. package/src/client/hooks/workspace/file-tabs.ts +5 -1
  91. package/src/client/hooks/workspace/repo-scope/index.ts +119 -0
  92. package/src/client/hooks/workspace/repo-scope/store.test.ts +161 -0
  93. package/src/client/hooks/workspace/repo-scope/store.ts +201 -0
  94. package/src/client/hooks/workspace/search-stream.ts +24 -5
  95. package/src/client/main.tsx +6 -0
  96. package/src/client/tailwind.css +19 -56
  97. package/src/server/lib/btw/child.server.ts +41 -0
  98. package/src/server/lib/btw/lifecycle.server.ts +254 -0
  99. package/src/server/lib/btw/prompt.ts +13 -5
  100. package/src/server/lib/btw/registry.server.ts +13 -4
  101. package/src/server/lib/btw/runtime.server.ts +149 -156
  102. package/src/server/lib/btw/schema.server.ts +27 -0
  103. package/src/server/lib/btw/service.server.ts +106 -5
  104. package/src/server/lib/btw/store.server.ts +57 -6
  105. package/src/server/lib/db/schema.ts +4 -0
  106. package/src/server/lib/db/settings-store.test.ts +65 -0
  107. package/src/server/lib/db/settings-store.ts +25 -0
  108. package/src/server/lib/fs/atomic-write.ts +42 -4
  109. package/src/server/lib/fs/config-lock.ts +74 -0
  110. package/src/server/lib/fs/git-ignore.ts +55 -5
  111. package/src/server/lib/fs/ripgrep.ts +119 -0
  112. package/src/server/lib/lifecycle/identity.ts +10 -64
  113. package/src/server/lib/lifecycle/proc/darwin.ts +238 -0
  114. package/src/server/lib/lifecycle/proc/index.ts +38 -0
  115. package/src/server/lib/lifecycle/proc/linux.ts +80 -0
  116. package/src/server/lib/lifecycle/proc/ps.ts +86 -0
  117. package/src/server/lib/lifecycle/proc/types.ts +33 -0
  118. package/src/server/lib/lifecycle/proc/win32.ts +53 -0
  119. package/src/server/lib/models/provider-registry.server.ts +2 -2
  120. package/src/server/lib/models/registry.server.ts +1 -1
  121. package/src/server/lib/omp/config/behavior.ts +17 -17
  122. package/src/server/lib/omp/config/disabled-providers.ts +84 -35
  123. package/src/server/lib/omp/config/document.ts +128 -0
  124. package/src/server/lib/omp/config/extensions.ts +11 -10
  125. package/src/server/lib/omp/config/mcp.ts +4 -57
  126. package/src/server/lib/omp/config/model-overrides.ts +123 -0
  127. package/src/server/lib/omp/config/models-config.ts +92 -0
  128. package/src/server/lib/omp/config/models-validation.ts +159 -0
  129. package/src/server/lib/omp/config/path-scoped.ts +74 -0
  130. package/src/server/lib/omp/config/providers.ts +241 -211
  131. package/src/server/lib/omp/config/roles.ts +6 -14
  132. package/src/server/lib/omp/core/auth-credentials.ts +1 -1
  133. package/src/server/lib/omp/rpc/frame-fold.test.ts +124 -0
  134. package/src/server/lib/omp/rpc/frame-fold.ts +43 -24
  135. package/src/server/lib/omp/rpc/lifecycle.test.ts +41 -0
  136. package/src/server/lib/omp/rpc/manager.ts +21 -21
  137. package/src/server/lib/omp/rpc/pending-ui-dialogs.ts +11 -7
  138. package/src/server/lib/omp/rpc/session-commands.ts +17 -29
  139. package/src/server/lib/omp/rpc/session-registry.ts +5 -66
  140. package/src/server/lib/omp/rpc/web-state.ts +0 -6
  141. package/src/server/lib/omp/session/auto-title.server.test.ts +109 -11
  142. package/src/server/lib/omp/session/auto-title.server.ts +95 -20
  143. package/src/server/lib/omp/session/sidebar-data.server.ts +11 -6
  144. package/src/server/lib/queue/delivery-dispatch.server.test.ts +162 -0
  145. package/src/server/lib/queue/delivery-harness.ts +88 -0
  146. package/src/server/lib/queue/delivery.server.test.ts +78 -149
  147. package/src/server/lib/queue/delivery.server.ts +105 -13
  148. package/src/server/lib/queue/store.server.ts +30 -5
  149. package/src/server/lib/terminal/foreground.test.ts +164 -0
  150. package/src/server/lib/terminal/foreground.ts +34 -6
  151. package/src/server/lib/terminal/history.ts +8 -7
  152. package/src/server/plugins/compress.ts +33 -5
  153. package/src/server/plugins/ssr.ts +14 -7
  154. package/src/server/routes/btw/topics.ts +89 -2
  155. package/src/server/routes/fs/git.ts +5 -6
  156. package/src/server/routes/fs/search.ts +17 -26
  157. package/src/server/routes/sessions/queue.ts +17 -4
  158. package/src/server/routes/settings/index.ts +2 -0
  159. package/src/server/routes/settings/model-override.ts +69 -0
  160. package/src/server/routes/settings/provider-models.ts +35 -12
  161. package/src/server/routes/settings/providers.ts +31 -1
  162. package/src/server/routes/settings/root.ts +8 -3
  163. package/src/shared/lib/chat/btw/client.ts +34 -0
  164. package/src/shared/lib/chat/timeline/tool-phrases.ts +7 -0
  165. package/src/shared/lib/code/shiki-themes.ts +9 -5
  166. package/src/shared/lib/fs/file-tree.ts +17 -12
  167. package/src/shared/lib/markdown/autolink.test.ts +32 -0
  168. package/src/shared/lib/markdown/autolink.ts +50 -0
  169. package/src/shared/lib/markdown/document-urls.test.ts +68 -0
  170. package/src/shared/lib/markdown/document-urls.ts +118 -0
  171. package/src/shared/lib/markdown/marked.ts +27 -15
  172. package/src/shared/lib/markdown/mermaid.ts +2 -2
  173. package/src/shared/lib/models/catalog.ts +1 -1
  174. package/src/shared/lib/omp/session/schema-migrations.ts +30 -0
  175. package/src/shared/lib/omp/session/stream-state.server.test.ts +54 -0
  176. package/src/shared/lib/omp/session/stream-state.server.ts +70 -21
  177. package/src/shared/lib/settings/client.ts +10 -4
  178. package/src/shared/lib/settings/diff.test.ts +31 -0
  179. package/src/shared/lib/settings/diff.ts +37 -0
  180. package/src/shared/lib/theme/catalog.test.ts +168 -0
  181. package/src/shared/lib/theme/catalog.ts +98 -0
  182. package/src/shared/lib/theme/color.ts +88 -0
  183. package/src/shared/lib/theme/css.ts +70 -0
  184. package/src/shared/lib/theme/palettes/chamber.ts +61 -0
  185. package/src/shared/lib/theme/palettes/dark.ts +313 -0
  186. package/src/shared/lib/theme/palettes/light.ts +313 -0
  187. package/src/shared/types/btw.ts +83 -4
  188. package/src/shared/types/index.ts +1 -0
  189. package/src/shared/types/settings/provider.ts +8 -1
  190. package/src/shared/types/settings/state.ts +10 -4
  191. package/src/shared/types/theme.ts +48 -0
  192. package/dist/client/static/css/index.b938f4a693.css +0 -2
  193. package/dist/client/static/js/77.8022a800b5.js +0 -139
  194. package/dist/client/static/js/async/1k.a411fe5b43.js +0 -2
  195. package/dist/client/static/js/async/66.86544ad741.js +0 -93
  196. package/dist/client/static/js/async/7g.db82aca7ba.js +0 -1
  197. package/dist/client/static/js/async/ar.ad5c8822a8.js +0 -2
  198. package/dist/client/static/js/async/e.6443e739a2.js +0 -79
  199. package/dist/client/static/js/async/g.ee41bfed7f.js +0 -2
  200. package/dist/client/static/js/async/h1.d7091cef78.js +0 -1
  201. package/dist/client/static/js/async/jl.4911fa3383.js +0 -1
  202. package/dist/client/static/js/async/mt.853be2b5aa.js +0 -1
  203. package/dist/client/static/js/async/ot.84c71b308f.js +0 -2
  204. package/dist/client/static/js/async/px.783b6f91f2.js +0 -1
  205. package/dist/client/static/js/async/q8.7f1b48e9cd.js +0 -21
  206. package/dist/client/static/js/async/q8.7f1b48e9cd.js.LICENSE.txt +0 -6
  207. package/dist/client/static/js/async/s.aad2ecebc6.js +0 -94
  208. package/dist/client/static/js/async/vt.20406a5604.js +0 -1
  209. package/dist/client/static/js/async/x1.fe8af17016.js +0 -90
  210. package/dist/client/static/js/async/xn.6360379f71.js.LICENSE.txt +0 -6
  211. package/dist/client/static/js/async/yg.5a94e1fc8a.js +0 -149
  212. package/dist/client/static/js/async/yv.2fa22f009a.js +0 -1
  213. package/dist/client/static/js/index.5fe7a2cb64.js +0 -34
  214. package/src/client/components/workspace/btw-panel/Composer.tsx +0 -183
  215. package/src/client/components/workspace/btw-panel/attachments.ts +0 -80
  216. /package/dist/client/static/js/{77.8022a800b5.js.LICENSE.txt → 77.0d81c74cf0.js.LICENSE.txt} +0 -0
  217. /package/dist/client/static/js/async/{g.ee41bfed7f.js.LICENSE.txt → 1k.973e8e21fc.js.LICENSE.txt} +0 -0
  218. /package/dist/client/static/js/async/{3y.3186ecdd63.js.LICENSE.txt → 3y.3530b824ad.js.LICENSE.txt} +0 -0
  219. /package/dist/client/static/js/async/{ot.84c71b308f.js.LICENSE.txt → 4m.928a1dde85.js.LICENSE.txt} +0 -0
  220. /package/dist/client/static/js/async/{q.b453193538.js.LICENSE.txt → ar.9303e5f2e8.js.LICENSE.txt} +0 -0
  221. /package/dist/client/static/js/async/{1k.a411fe5b43.js.LICENSE.txt → q.33d6779f98.js.LICENSE.txt} +0 -0
  222. /package/dist/client/static/js/async/{ar.ad5c8822a8.js.LICENSE.txt → xn.6510c0abe2.js.LICENSE.txt} +0 -0
  223. /package/dist/client/static/js/{index.5fe7a2cb64.js.LICENSE.txt → index.c05535dbde.js.LICENSE.txt} +0 -0
@@ -1,28 +1,71 @@
1
1
  import { useEffect, useRef, useState } from 'preact/hooks';
2
2
  import { RefreshCw, Search } from 'lucide-preact';
3
+ import type { FsNode } from '@/shared/types';
3
4
  import { rehydrateTree, setChildrenAt } from '@/shared/lib/fs/file-tree';
5
+ import { isRecord } from '@/shared/lib/util/guards';
4
6
  import { GitRepoDropdown } from '@/client/components/workspace/file-explorer/GitRepoDropdown';
5
7
  import { FileTreeItem } from '@/client/components/workspace/file-explorer/TreeItem';
6
8
  import { useScrollbarFade, scrollbarFadeClass } from '@/client/hooks/ui/scrollbar-fade';
7
9
  import { useSessionState } from '@/client/hooks/workspace/session-state';
8
10
  import { useGitStatus } from '@/client/hooks/workspace/git-status';
11
+ import { useRepoList, useRepoScope } from '@/client/hooks/workspace/repo-scope';
9
12
  import { usePanelRefresh, useFileMutationRefresh } from '@/client/hooks/workspace/panel-refresh';
10
13
 
14
+ /**
15
+ * The listing on screen, tagged with the `root\0repo` scope it was read for.
16
+ *
17
+ * Tagging it is what makes a workspace switch instant and complete: the panel
18
+ * renders an empty tree until the new root's listing lands, instead of showing
19
+ * the previous workspace's files under the new workspace's header. It also
20
+ * covers what a plain "reset on switch" effect cannot — the children loaded on
21
+ * demand and the expansion set are keyed by paths RELATIVE to the listed root,
22
+ * so `src/` in one workspace would rehydrate into `src/` in the next.
23
+ */
24
+ interface Listing {
25
+ scope: string;
26
+ files: FsNode[];
27
+ /** Absolute base dir reported by `/api/fs/dir`; Copy Path anchors on it. */
28
+ root: string;
29
+ }
30
+
31
+ const EMPTY_LISTING: Listing = { scope: '', files: [], root: '' };
32
+
33
+ /** Directory listing payload from `/api/fs/dir`, validated field by field. */
34
+ function readDirPayload(data: unknown): { files: FsNode[]; root: string } {
35
+ if (!isRecord(data)) return { files: [], root: '' };
36
+ return {
37
+ files: Array.isArray(data.files) ? data.files.filter((f): f is FsNode => isRecord(f)) : [],
38
+ root: typeof data.root === 'string' ? data.root : '',
39
+ };
40
+ }
41
+
11
42
  export function FileExplorer({ className = '', enabled = true, rootPath, onOpenFile, refreshKey = 0, onRefresh }: { className?: string, enabled?: boolean, rootPath?: string, onOpenFile?: (file: any) => void, refreshKey?: number, onRefresh?: () => void }) {
12
- const [tree, setTree] = useState<any[]>([]);
13
- // Absolute base dir reported by `/api/fs/dir` for the current listing — the
14
- // workspace root, or the selected nested repo. Copy Path anchors on it
15
- // because the client's `rootPath` may be rejected/unset.
16
- const [listingRoot, setListingRoot] = useState('');
43
+ const [listing, setListing] = useState<Listing>(EMPTY_LISTING);
17
44
  const [searchQuery, setSearchQuery] = useSessionState<string>('files.searchQuery', '');
18
45
  const [isLoading, setIsLoading] = useState(false);
19
46
  const [storedExpandedPaths, setStoredExpandedPaths, expandedPathsReady] = useSessionState<string[]>('files.expandedPaths', []);
20
47
  const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set(storedExpandedPaths));
21
- const childrenCacheRef = useRef<Record<string, any[]>>({});
22
- const [activeRepo, setActiveRepo] = useSessionState<string>('files.activeRepo', '.');
48
+ const childrenCacheRef = useRef<Record<string, FsNode[]>>({});
49
+ const { activeRepo, setActiveRepo } = useRepoScope(rootPath, 'files.activeRepo');
50
+ const { repos, scanning: reposScanning, rescan: rescanRepos } = useRepoList(rootPath, enabled);
23
51
  const { isScrolling, handleScroll } = useScrollbarFade();
24
52
  const { fileMap: gitFileMap, folderMap: gitFolderMap, refreshGitStatus } = useGitStatus(rootPath, activeRepo, refreshKey, enabled);
25
53
 
54
+ // The scope every request and every cached child path below belongs to. A
55
+ // change to it means the previous workspace's or repo's entries are not this
56
+ // one's, so nothing read under the old value may be rendered.
57
+ const scope = `${rootPath ?? ''}\u0000${activeRepo}`;
58
+ const scopeRef = useRef(scope);
59
+ scopeRef.current = scope;
60
+ // The scope of the listing on screen, read outside the render closure so the
61
+ // async loaders can tell a superseded tree from the current one.
62
+ const listedScopeRef = useRef(EMPTY_LISTING.scope);
63
+
64
+ const commitListing = (next: Listing) => {
65
+ listedScopeRef.current = next.scope;
66
+ setListing(next);
67
+ };
68
+
26
69
  useEffect(() => {
27
70
  if (!expandedPathsReady) return;
28
71
  setExpandedPaths(prev => {
@@ -43,14 +86,23 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
43
86
 
44
87
  const loadFiles = (opts?: { silent?: boolean }) => {
45
88
  if (!enabled) return;
89
+ const requested = scopeRef.current;
46
90
  if (!opts?.silent) setIsLoading(true);
47
91
  fetch(listUrl())
48
92
  .then(r => r.json())
49
93
  .then(data => {
50
- if (typeof data.root === 'string') setListingRoot(data.root);
51
- if (Array.isArray(data.files)) {
52
- setTree(rehydrateTree(data.files, childrenCacheRef.current, expandedPaths));
53
- }
94
+ // A read that was superseded by a workspace switch describes a tree
95
+ // this panel is no longer showing.
96
+ if (scopeRef.current !== requested) return;
97
+ // Children cached under the previous tree are keyed by paths relative
98
+ // to a root that is not this one: drop them before rehydrating.
99
+ if (listedScopeRef.current !== requested) childrenCacheRef.current = {};
100
+ const payload = readDirPayload(data);
101
+ commitListing({
102
+ scope: requested,
103
+ files: rehydrateTree(payload.files, childrenCacheRef.current),
104
+ root: payload.root,
105
+ });
54
106
  })
55
107
  .catch(() => {})
56
108
  .finally(() => {
@@ -85,13 +137,17 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
85
137
  // so a freshly listed row never renders bare next to a status map that
86
138
  // still predates its change (the panel poll alone can lag a fresh edit).
87
139
  refreshGitStatus();
140
+ const requested = scopeRef.current;
88
141
  return fetch(listUrl(path))
89
142
  .then(r => r.json())
90
143
  .then(data => {
91
- if (Array.isArray(data.files)) {
92
- childrenCacheRef.current[path] = data.files;
93
- setTree(prev => setChildrenAt(prev, path, data.files));
94
- }
144
+ if (scopeRef.current !== requested) return;
145
+ const payload = readDirPayload(data);
146
+ if (payload.files.length === 0 && payload.root === '') return;
147
+ childrenCacheRef.current[path] = payload.files;
148
+ setListing(prev => prev.scope === requested
149
+ ? { ...prev, files: setChildrenAt(prev.files, path, payload.files) }
150
+ : prev);
95
151
  })
96
152
  .catch(() => {});
97
153
  };
@@ -106,6 +162,10 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
106
162
 
107
163
  const handleSelectRepo = (repo: string) => {
108
164
  if (repo === activeRepo) return;
165
+ // The expansion set holds paths relative to the repo that was listed, so a
166
+ // repo switch starts from a collapsed tree. (A workspace switch needs no
167
+ // equivalent: the listing is re-read under the new root, and the session's
168
+ // own expansion set is restored with the session.)
109
169
  childrenCacheRef.current = {};
110
170
  setExpandedPaths(new Set());
111
171
  setStoredExpandedPaths([]);
@@ -120,6 +180,9 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
120
180
  );
121
181
  }
122
182
 
183
+ const tree = listing.scope === scope ? listing.files : [];
184
+ const listingRoot = listing.scope === scope ? listing.root : '';
185
+
123
186
  const getFilteredFiles = () => {
124
187
  if (!searchQuery.trim()) return tree;
125
188
 
@@ -158,7 +221,14 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
158
221
  <div className={`flex flex-col h-full bg-paper ${className}`}>
159
222
  <div className="p-3 border-b border-ink/10 flex flex-col space-y-2">
160
223
  <div className="flex items-center justify-between">
161
- <GitRepoDropdown rootPath={rootPath} activeRepo={activeRepo} onSelectRepo={handleSelectRepo} />
224
+ <GitRepoDropdown
225
+ rootPath={rootPath}
226
+ activeRepo={activeRepo}
227
+ onSelectRepo={handleSelectRepo}
228
+ repos={repos}
229
+ scanning={reposScanning}
230
+ onRefreshRepos={rescanRepos}
231
+ />
162
232
  <button
163
233
  onClick={refresh}
164
234
  className="p-1.5 text-ink/40 hover:text-ink hover:bg-ink/5 rounded transition-colors"
@@ -10,8 +10,8 @@ import { GitChangesList } from '@/client/components/workspace/git-panel/ChangesL
10
10
  import { ToastStack } from '@/client/components/common/ToastStack';
11
11
  import { useToasts } from '@/client/hooks/ui/toasts';
12
12
  import { useSessionState } from '@/client/hooks/workspace/session-state';
13
+ import { resolveRepoForPanel, useRepoList, useRepoScope } from '@/client/hooks/workspace/repo-scope';
13
14
  import { usePanelRefresh, useFileMutationRefresh } from '@/client/hooks/workspace/panel-refresh';
14
- import { REPO_DISCOVERY_POLL_MS } from '@/shared/lib/workspace/refresh-cadence';
15
15
  import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
16
16
 
17
17
  interface GitPanelProps {
@@ -22,15 +22,46 @@ interface GitPanelProps {
22
22
  }
23
23
 
24
24
  export function GitPanel({ className = '', enabled = true, rootPath, refreshKey = 0 }: GitPanelProps) {
25
- const fetcher = useFetcher<{ changes: GitChange[], branch: string, branches: string[], remoteBranches?: string[], repos: string[], reposPending?: boolean, activeRepo: string, syncCount?: { ahead: number, behind: number } }>();
25
+ const fetcher = useFetcher<{ changes: GitChange[], branch: string, branches: string[], remoteBranches?: string[], syncCount?: { ahead: number, behind: number } }>();
26
26
  const actionFetcher = useFetcher<{ success: boolean, type?: string, data?: any, error?: string }>();
27
27
 
28
- const [storedActiveRepo, setStoredActiveRepo, activeRepoReady] = useSessionState<string>('git.activeRepo', '.');
29
- const activeRepo = fetcher.data?.activeRepo || storedActiveRepo || '.';
28
+ const { activeRepo: pickedRepo, setActiveRepo, ready: activeRepoReady } = useRepoScope(rootPath, 'git.activeRepo');
29
+ const { repos, scanning: reposScanning, rescan: refreshRepos } = useRepoList(rootPath, enabled);
30
+ // When the user has picked nothing and the workspace root is not itself a
31
+ // repo, git falls back to the first repo discovery found — the same choice
32
+ // the loader makes for a request that names no repo. Resolving it here rather
33
+ // than reading it back from the response is what keeps the header label, the
34
+ // picker's checkmark and the requests describing one repo: the response
35
+ // arrives a render later, and by then it may belong to a workspace the user
36
+ // has already left.
37
+ const activeRepo = resolveRepoForPanel(pickedRepo, repos);
38
+ const activeRepoRef = useRef(activeRepo);
39
+ activeRepoRef.current = activeRepo;
40
+
41
+ // `root\0repo` — the working tree the panel is showing right now.
42
+ const scopeRef = useRef('');
43
+ scopeRef.current = `${rootPath ?? ''}\u0000${activeRepo}`;
44
+ // The scope the in-flight read was asked for, and the scope the payload
45
+ // currently in `fetcher.data` was asked for. Only one read is ever in flight
46
+ // (the fetcher aborts its predecessor), so the former is what the latter
47
+ // becomes when a new payload arrives.
48
+ const requestedScopeRef = useRef('');
49
+ const payloadScopeRef = useRef('');
50
+ const lastPayloadRef = useRef<unknown>(undefined);
51
+ if (fetcher.data !== lastPayloadRef.current) {
52
+ lastPayloadRef.current = fetcher.data;
53
+ payloadScopeRef.current = requestedScopeRef.current;
54
+ }
55
+ // A switch leaves the previous repo's branch, changes and branch list in
56
+ // `fetcher.data` until the new read lands. Rendering those under the new
57
+ // repo's header would claim the new working tree is on the old branch with
58
+ // the old changes, so the payload is used only while it describes the scope
59
+ // on screen.
60
+ const data = payloadScopeRef.current === scopeRef.current ? fetcher.data : undefined;
30
61
 
31
62
  const loadRepo = (repo?: string) => {
32
63
  if (!enabled) return;
33
- const targetRepo = repo !== undefined ? repo : storedActiveRepo;
64
+ const targetRepo = repo ?? activeRepoRef.current;
34
65
  const params = new URLSearchParams();
35
66
  if (rootPath) params.set('root', rootPath);
36
67
  if (targetRepo && targetRepo !== '.') params.set('repo', targetRepo);
@@ -39,6 +70,11 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
39
70
  // numbers describe origin rather than the last fetch.
40
71
  params.set('sync', '1');
41
72
  params.set('t', String(Date.now()));
73
+ // Tag the response with the scope it was asked for: a switch leaves the
74
+ // previous repo's branch, changes and branch list in `fetcher.data` until
75
+ // the new read lands, and rendering those under the new repo's header
76
+ // claims the new working tree is on the old branch with the old changes.
77
+ requestedScopeRef.current = scopeRef.current;
42
78
  fetcher.load(`/api/fs/git?${params.toString()}`);
43
79
  };
44
80
 
@@ -66,70 +102,26 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
66
102
 
67
103
  const { toasts, pushToast, dismissToast } = useToasts();
68
104
 
69
- // Background nested-repo discovery polling: the loader returns immediately
70
- // with the root status and `reposPending`; we poll the lightweight
71
- // `?reposOnly=1` endpoint until the discoverer finishes, then refresh.
72
- const [extraRepos, setExtraRepos] = useState<string[] | null>(null);
73
- const [pollingRepos, setPollingRepos] = useState(false);
74
- const [rescanningRepos, setRescanningRepos] = useState(false);
75
-
76
105
  useEffect(() => {
77
106
  setMounted(true);
78
107
  }, []);
79
108
 
109
+ // `activeRepo` is a dependency, not just the root: the repo list that
110
+ // resolves the root-level fallback arrives after the first render, and a
111
+ // workspace switch re-resolves it to '.'.
80
112
  useEffect(() => {
81
113
  if (!activeRepoReady) return;
82
- loadRepo(storedActiveRepo);
83
- }, [refreshKey, rootPath, enabled, activeRepoReady]);
114
+ loadRepo();
115
+ }, [refreshKey, rootPath, enabled, activeRepoReady, activeRepo]);
84
116
 
85
117
  // Auto refresh: keep the change list in step with external mutations
86
118
  // (terminal commits, agent edits) that never bump `refreshKey`. `loadRepo`
87
119
  // re-reads via `fetcher.load`, whose `t` param busts the cache; a load
88
120
  // already in flight is ignored by the loader so polls cannot pile up.
89
- usePanelRefresh(() => loadRepo(storedActiveRepo), enabled && activeRepoReady);
121
+ usePanelRefresh(() => loadRepo(), enabled && activeRepoReady);
90
122
  // Agent edits/commits land between poll ticks: re-read right after a
91
123
  // file-mutating tool finishes so the change list tracks the AI's work.
92
- useFileMutationRefresh(() => loadRepo(storedActiveRepo), enabled && activeRepoReady);
93
-
94
- useEffect(() => {
95
- if (fetcher.data?.activeRepo && fetcher.data.activeRepo !== storedActiveRepo) {
96
- setStoredActiveRepo(fetcher.data.activeRepo);
97
- }
98
- }, [fetcher.data?.activeRepo, storedActiveRepo, setStoredActiveRepo]);
99
-
100
- useEffect(() => {
101
- if (fetcher.data?.reposPending) setPollingRepos(true);
102
- }, [fetcher.data]);
103
-
104
- // A forced rescan may finish before the polling interval kicks in; when that
105
- // happens the loader returns the fresh repo list synchronously.
106
- useEffect(() => {
107
- if (
108
- rescanningRepos &&
109
- fetcher.data &&
110
- !fetcher.data.reposPending &&
111
- Array.isArray(fetcher.data.repos)
112
- ) {
113
- setExtraRepos(fetcher.data.repos);
114
- setRescanningRepos(false);
115
- }
116
- }, [fetcher.data, rescanningRepos]);
117
-
118
- useEffect(() => {
119
- if (!pollingRepos) return;
120
- const params = new URLSearchParams({ reposOnly: '1' });
121
- if (rootPath) params.set('root', rootPath);
122
- const id = setInterval(async () => {
123
- const data = await fetch(`/api/fs/git?${params.toString()}`).then(r => r.json()).catch(() => null);
124
- if (data && !data.reposPending && Array.isArray(data.repos)) {
125
- setExtraRepos(data.repos);
126
- setPollingRepos(false);
127
- setRescanningRepos(false);
128
- loadRepo(storedActiveRepo);
129
- }
130
- }, REPO_DISCOVERY_POLL_MS);
131
- return () => clearInterval(id);
132
- }, [pollingRepos, rootPath, storedActiveRepo]);
124
+ useFileMutationRefresh(() => loadRepo(), enabled && activeRepoReady);
133
125
 
134
126
  // Focus input when branch prompt opens
135
127
  useEffect(() => {
@@ -146,7 +138,7 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
146
138
  } else if (actionFetcher.data.type === 'graph') {
147
139
  setViewingOutput({ title: 'Git Graph', data: actionFetcher.data.data || [] });
148
140
  } else if (actionFetcher.data.success) {
149
- loadRepo(activeRepo);
141
+ loadRepo();
150
142
  } else {
151
143
  pushToast(actionFetcher.data.error || 'Git operation failed');
152
144
  }
@@ -188,11 +180,10 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
188
180
  }
189
181
  };
190
182
 
191
- const branch = fetcher.data?.branch || 'main';
192
- const branches = fetcher.data?.branches || ['main'];
193
- const remoteBranches = fetcher.data?.remoteBranches || [];
194
- const repos = extraRepos ?? (fetcher.data?.repos || ['.']);
195
- const changes = fetcher.data?.changes || [];
183
+ const branch = data?.branch || 'main';
184
+ const branches = data?.branches || ['main'];
185
+ const remoteBranches = data?.remoteBranches || [];
186
+ const changes = data?.changes || [];
196
187
  const isLoading = fetcher.state === 'loading' || actionFetcher.state !== 'idle';
197
188
 
198
189
  const stagedChanges = changes.filter(c => {
@@ -200,17 +191,6 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
200
191
  return status && status[0] !== ' ' && status[0] !== '?';
201
192
  });
202
193
 
203
- const refreshRepos = () => {
204
- if (!enabled || pollingRepos || rescanningRepos) return;
205
- setExtraRepos(repos);
206
- setRescanningRepos(true);
207
- const params = new URLSearchParams({ rescan: '1', sync: '1' });
208
- if (rootPath) params.set('root', rootPath);
209
- if (activeRepo !== '.') params.set('repo', activeRepo);
210
- params.set('t', String(Date.now()));
211
- fetcher.load(`/api/fs/git?${params.toString()}`);
212
- };
213
-
214
194
  if (!enabled) {
215
195
  return (
216
196
  <div className={`flex flex-col h-full bg-paper items-center justify-center text-ink/40 ${className}`}>
@@ -276,12 +256,14 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
276
256
  repos={repos}
277
257
  isLoading={isLoading}
278
258
  rootPath={rootPath}
279
- reposScanning={pollingRepos || rescanningRepos}
259
+ reposScanning={reposScanning}
280
260
  onSelectRepo={(r) => {
281
- setStoredActiveRepo(r);
282
- loadRepo(r);
261
+ // The reload is the scope effect's job: it fires on the resolved
262
+ // `activeRepo` change, so selecting here would fetch the same repo
263
+ // twice.
264
+ if (r !== activeRepo) setActiveRepo(r);
283
265
  }}
284
- onRefresh={() => loadRepo(activeRepo)}
266
+ onRefresh={() => loadRepo()}
285
267
  onRefreshRepos={refreshRepos}
286
268
  />
287
269
 
@@ -303,7 +285,7 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
303
285
  viewMode={viewMode}
304
286
  setViewMode={setViewMode}
305
287
  isBusy={isLoading}
306
- syncCount={fetcher.data?.syncCount}
288
+ syncCount={data?.syncCount}
307
289
  onSync={() => executeAction('sync')}
308
290
  />
309
291
 
@@ -8,6 +8,11 @@ interface TerminalHeaderProps {
8
8
  rootPath?: string;
9
9
  activeRepo: string;
10
10
  onSelectRepo: (repo: string) => void;
11
+ /** Discovered repos of the active root; the panel owns discovery. */
12
+ repos: string[];
13
+ /** Discovery or a forced rescan is in flight. */
14
+ reposScanning: boolean;
15
+ onRefreshRepos: () => void;
11
16
  /** Working directory the shell actually runs in. */
12
17
  cwd?: string;
13
18
  shell?: string;
@@ -35,6 +40,9 @@ export function TerminalHeader({
35
40
  rootPath,
36
41
  activeRepo,
37
42
  onSelectRepo,
43
+ repos,
44
+ reposScanning,
45
+ onRefreshRepos,
38
46
  cwd,
39
47
  shell,
40
48
  gitBranch,
@@ -60,7 +68,14 @@ export function TerminalHeader({
60
68
  <span>{status === 'exited' && exitCode !== null ? `EXIT ${exitCode}` : STATUS_LABEL[status]}</span>
61
69
  </span>
62
70
 
63
- <GitRepoDropdown rootPath={rootPath} activeRepo={activeRepo} onSelectRepo={onSelectRepo} />
71
+ <GitRepoDropdown
72
+ rootPath={rootPath}
73
+ activeRepo={activeRepo}
74
+ onSelectRepo={onSelectRepo}
75
+ repos={repos}
76
+ scanning={reposScanning}
77
+ onRefreshRepos={onRefreshRepos}
78
+ />
64
79
 
65
80
  <button
66
81
  type="button"
@@ -40,7 +40,7 @@ interface RealtimeXtermViewProps {
40
40
  */
41
41
  export const RealtimeXtermView = forwardRef<RealtimeXtermHandle, RealtimeXtermViewProps>(
42
42
  ({ onInput, onBinaryInput, onGridChange, onReady }, ref) => {
43
- const { isDark } = useTheme();
43
+ const { theme } = useTheme();
44
44
  const containerRef = useRef<HTMLDivElement>(null);
45
45
  const mountRef = useRef<XtermMount | null>(null);
46
46
  const [isScrolledUp, setIsScrolledUp] = useState(false);
@@ -53,15 +53,15 @@ export const RealtimeXtermView = forwardRef<RealtimeXtermHandle, RealtimeXtermVi
53
53
  gridChangeRef.current = onGridChange;
54
54
  const readyRef = useRef(onReady);
55
55
  readyRef.current = onReady;
56
- const themeRef = useRef(isDark);
57
- themeRef.current = isDark;
56
+ const themeRef = useRef(theme);
57
+ themeRef.current = theme;
58
58
 
59
59
  useEffect(() => {
60
60
  const term = mountRef.current?.term;
61
61
  if (!term) return;
62
- term.options.theme = getXtermTheme(isDark);
62
+ term.options.theme = getXtermTheme(theme);
63
63
  term.refresh(0, term.rows - 1);
64
- }, [isDark]);
64
+ }, [theme]);
65
65
 
66
66
  useImperativeHandle(ref, () => ({
67
67
  write(bytes: Uint8Array) {
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useRef } from 'preact/hooks';
2
2
  import { useTerminal } from '@/client/hooks/workspace/terminal';
3
- import { useSessionState } from '@/client/hooks/workspace/session-state';
3
+ import { useRepoList, useRepoScope } from '@/client/hooks/workspace/repo-scope';
4
4
  import { useTheme } from '@/client/hooks/ui/theme';
5
5
  import { TerminalHeader } from '@/client/components/workspace/terminal-panel/Header';
6
6
  import { TerminalQuickActions } from '@/client/components/workspace/terminal-panel/QuickActions';
@@ -18,7 +18,8 @@ const encoder = new TextEncoder();
18
18
  export function TerminalPanel({ className = '', enabled = true, rootPath, showHeader = true }: TerminalPanelProps) {
19
19
  const xtermRef = useRef<RealtimeXtermHandle>(null);
20
20
  const { isDark } = useTheme();
21
- const [activeRepo, setActiveRepo] = useSessionState<string>('terminal.activeRepo', '.');
21
+ const { activeRepo, setActiveRepo } = useRepoScope(rootPath, 'terminal.activeRepo');
22
+ const { repos, scanning: reposScanning, rescan: rescanRepos } = useRepoList(rootPath, enabled);
22
23
  const gridRef = useRef<{ cols: number; rows: number } | null>(null);
23
24
 
24
25
  const handleOutput = useCallback((bytes: Uint8Array) => {
@@ -105,6 +106,9 @@ export function TerminalPanel({ className = '', enabled = true, rootPath, showHe
105
106
  rootPath={rootPath}
106
107
  activeRepo={activeRepo}
107
108
  onSelectRepo={setActiveRepo}
109
+ repos={repos}
110
+ reposScanning={reposScanning}
111
+ onRefreshRepos={rescanRepos}
108
112
  cwd={cwd}
109
113
  shell={shell}
110
114
  gitBranch={systemInfo.gitBranch}
@@ -71,7 +71,7 @@ export interface XtermMount {
71
71
  */
72
72
  export async function mountXterm(
73
73
  container: HTMLElement,
74
- isDark: boolean,
74
+ themeId: string,
75
75
  callbacks: XtermMountCallbacks,
76
76
  isCancelled: () => boolean,
77
77
  ): Promise<XtermMount | null> {
@@ -93,7 +93,7 @@ export async function mountXterm(
93
93
  fontSize: 12,
94
94
  lineHeight: 1.25,
95
95
  fontFamily: XTERM_FONT_FAMILY,
96
- theme: getXtermTheme(isDark),
96
+ theme: getXtermTheme(themeId),
97
97
  // No `convertEol`: the PTY already emits CRLF, and converting a lone LF
98
98
  // would re-introduce the line-feed doubling a real terminal avoids.
99
99
  scrollback: 10000,
@@ -1,58 +1,56 @@
1
- export const XTERM_LIGHT_THEME = {
2
- background: '#f4f1ea',
3
- foreground: '#141310',
4
- cursor: '#141310',
5
- cursorAccent: '#f4f1ea',
6
- selectionBackground: '#ded8ce',
7
- selectionForeground: '#141310',
8
- black: '#141310',
9
- red: '#c8321e',
10
- green: '#047857',
11
- yellow: '#b45309',
12
- blue: '#1d4ed8',
13
- magenta: '#9333ea',
14
- cyan: '#0284c7',
15
- white: '#faf8f3',
16
- brightBlack: '#78716c',
17
- brightRed: '#dc2626',
18
- brightGreen: '#059669',
19
- brightYellow: '#d97706',
20
- brightBlue: '#2563eb',
21
- brightMagenta: '#a855f7',
22
- brightCyan: '#0ea5e9',
23
- brightWhite: '#292524',
24
- };
1
+ import { mixHex } from '@/shared/lib/theme/color';
2
+ import { resolveTheme } from '@/shared/lib/theme/catalog';
25
3
 
26
- export const XTERM_DARK_THEME = {
27
- background: '#21252b',
28
- foreground: '#abb2bf',
29
- cursor: '#528bff',
30
- cursorAccent: '#21252b',
31
- selectionBackground: '#3e4451',
32
- selectionForeground: '#abb2bf',
33
- black: '#282c34',
34
- red: '#e06c75',
35
- green: '#98c379',
36
- yellow: '#e5c07b',
37
- blue: '#61afef',
38
- magenta: '#c678dd',
39
- cyan: '#56b6c2',
40
- white: '#abb2bf',
41
- brightBlack: '#5c6370',
42
- brightRed: '#e06c75',
43
- brightGreen: '#98c379',
44
- brightYellow: '#e5c07b',
45
- brightBlue: '#61afef',
46
- brightMagenta: '#c678dd',
47
- brightCyan: '#56b6c2',
48
- brightWhite: '#ffffff',
49
- };
4
+ /**
5
+ * The terminal palette for a theme, derived from the same eight colors the rest
6
+ * of the app paints with.
7
+ *
8
+ * xterm parses its own colors and understands only literal notations (hex,
9
+ * `rgb()`, `hsl()`, named) — `color-mix()` is not among them in either the DOM
10
+ * or the WebGL renderer — so every blend is computed here in hex instead of
11
+ * being left to CSS. The result follows the active theme rather than a fixed
12
+ * light/dark pair: a Nord terminal is Nord, and switching themes repaints the
13
+ * scrollback instead of leaving the previous palette on screen.
14
+ *
15
+ * ANSI slots map onto the semantic colors the palette does carry (red=error,
16
+ * green=success, yellow=warning, blue=info, magenta=accent); `cyan` has no
17
+ * counterpart, so it is a deterministic mix of info and success, and the
18
+ * grayscale slots are mixes of ink over canvas. "Bright" variants mix 20% toward
19
+ * the foreground, which is lighter on a dark theme and darker on a light one —
20
+ * the direction that increases contrast on both.
21
+ */
22
+ export function getXtermTheme(themeId: string) {
23
+ const palette = resolveTheme(themeId);
24
+ const { canvas, ink } = palette;
50
25
 
51
- export function getXtermTheme(isDark: boolean) {
52
- return isDark ? XTERM_DARK_THEME : XTERM_LIGHT_THEME;
53
- }
26
+ const towardInk = (color: string) => mixHex(color, ink, 0.2);
27
+ const grayscale = (ratio: number) => mixHex(canvas, ink, ratio);
54
28
 
55
- export const XTERM_THEME = XTERM_DARK_THEME;
29
+ return {
30
+ background: canvas,
31
+ foreground: ink,
32
+ cursor: palette.meta,
33
+ cursorAccent: canvas,
34
+ selectionBackground: grayscale(0.28),
35
+ selectionForeground: ink,
36
+ black: grayscale(0.2),
37
+ red: palette.error,
38
+ green: palette.success,
39
+ yellow: palette.warning,
40
+ blue: palette.info,
41
+ magenta: palette.meta,
42
+ cyan: mixHex(palette.info, palette.success, 0.5),
43
+ white: grayscale(0.75),
44
+ brightBlack: grayscale(0.5),
45
+ brightRed: towardInk(palette.error),
46
+ brightGreen: towardInk(palette.success),
47
+ brightYellow: towardInk(palette.warning),
48
+ brightBlue: towardInk(palette.info),
49
+ brightMagenta: towardInk(palette.meta),
50
+ brightCyan: towardInk(mixHex(palette.info, palette.success, 0.5)),
51
+ brightWhite: grayscale(0.92),
52
+ };
53
+ }
56
54
 
57
55
  /**
58
56
  * Font stack for the terminal canvas.