@kabmo-labs/kabmo 0.8.0 → 0.9.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.
- package/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +314 -314
- package/dist/client/static/js/{index-qwpvd9fv.js → index-bpptfkz2.js} +922 -892
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +9 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +47 -67
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/mobile/LayoutWrapper.tsx +1 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +8 -2
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +16 -8
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +8 -11
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +19 -24
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +2 -0
- package/src/client/hooks/ui/updates/shell-download.ts +55 -14
- package/src/client/hooks/ui/updates/shell-offer.test.ts +26 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +20 -5
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +37 -8
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/fs/worktree/create.ts +2 -3
- package/src/server/lib/fs/worktree/list.ts +13 -0
- package/src/server/lib/fs/worktree/remove.ts +3 -3
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.ts +94 -3
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +33 -2
- package/src/server/lib/updates/shell-pages.ts +17 -5
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +10 -27
- package/src/server/lib/updates/shell-release.ts +107 -111
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +29 -7
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/list.ts +11 -1
- package/src/server/routes/worktrees/routes.test.ts +17 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +4 -4
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-gtbpewr7.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/chat-timeline/target-picker/index.test.tsx +0 -227
- package/src/client/components/workspace/chat-timeline/target-picker/index.tsx +0 -260
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The code surface's type and chrome, in CodeMirror's own theme format.
|
|
8
|
+
*
|
|
9
|
+
* Type is INHERITED, not restated: the host element carries the font family,
|
|
10
|
+
* size and line box (the zoom control and the editor-font setting both write
|
|
11
|
+
* there), and `.cm-editor` inherits them. That is what keeps a zoom step a pure
|
|
12
|
+
* CSS change — a theme extension carrying a pixel size would have to be
|
|
13
|
+
* reconfigured on every step, and every reconfigure re-creates the editor's
|
|
14
|
+
* plugins (including the tokenizer).
|
|
15
|
+
*
|
|
16
|
+
* Colours are read through `--editor-*` custom properties with the app's
|
|
17
|
+
* palette as the FALLBACK:
|
|
18
|
+
*
|
|
19
|
+
* - `auto` (the default) writes none of them, so every declaration resolves to
|
|
20
|
+
* `--theme-*` and an app theme flip repaints the editor with zero JS — the
|
|
21
|
+
* same property the Shiki decorations rely on.
|
|
22
|
+
* - An explicit editor theme writes them on the surface's own host element
|
|
23
|
+
* (`editorThemeVars`), so the editor paints Dracula while the console around
|
|
24
|
+
* it stays on the app palette. Because the values arrive as CSS, switching
|
|
25
|
+
* between two editor themes is also a repaint: nothing here is reconfigured.
|
|
26
|
+
*
|
|
27
|
+
* The one rule that cannot be a variable is the TOKEN colour. Shiki emits both
|
|
28
|
+
* palettes on every token, and which one paints is decided by the app's variant
|
|
29
|
+
* attribute — except an explicit editor theme tokenizes with ITSELF in both
|
|
30
|
+
* slots, so the two custom properties carry identical values and either one is
|
|
31
|
+
* correct. That is why the dark rule below is safe to state unconditionally.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { Extension } from '@codemirror/state';
|
|
35
|
+
import { EditorView } from '@codemirror/view';
|
|
36
|
+
|
|
37
|
+
/** The surface's own CSS. Everything here is structural, never per-layout. */
|
|
38
|
+
export function surfaceTheme(): Extension {
|
|
39
|
+
return EditorView.theme({
|
|
40
|
+
'&': {
|
|
41
|
+
height: '100%',
|
|
42
|
+
backgroundColor: 'var(--editor-bg, transparent)',
|
|
43
|
+
color: 'var(--editor-fg, var(--theme-ink))',
|
|
44
|
+
fontSize: 'inherit',
|
|
45
|
+
fontFamily: 'inherit',
|
|
46
|
+
lineHeight: 'inherit',
|
|
47
|
+
},
|
|
48
|
+
'&.cm-focused': { outline: 'none' },
|
|
49
|
+
'.cm-scroller': {
|
|
50
|
+
overflow: 'auto',
|
|
51
|
+
fontFamily: 'inherit',
|
|
52
|
+
fontSize: 'inherit',
|
|
53
|
+
lineHeight: 'inherit',
|
|
54
|
+
},
|
|
55
|
+
'.cm-content': {
|
|
56
|
+
// CM6 reads these two back as the document's padding, which is why the
|
|
57
|
+
// breathing room belongs here and not on a wrapper: a scroll container's
|
|
58
|
+
// own padding is not part of its scrollable overflow.
|
|
59
|
+
paddingTop: 'var(--surface-pad, 16px)',
|
|
60
|
+
paddingBottom: 'var(--surface-pad, 16px)',
|
|
61
|
+
caretColor: 'var(--editor-cursor, var(--theme-ink))',
|
|
62
|
+
},
|
|
63
|
+
'.cm-line': { padding: '0 var(--surface-pad, 16px)' },
|
|
64
|
+
// The gutter is CM6's own (lineNumbers()), styled to the panel's band. An
|
|
65
|
+
// explicit editor theme paints it in the theme's own surface, which is what
|
|
66
|
+
// VS Code does for the 46 themes that ship no `editorGutter.background`.
|
|
67
|
+
'.cm-gutters': {
|
|
68
|
+
backgroundColor: 'var(--editor-bg, var(--theme-canvas))',
|
|
69
|
+
color: 'var(--editor-gutter-fg, color-mix(in srgb, var(--theme-ink) 30%, transparent))',
|
|
70
|
+
border: 'none',
|
|
71
|
+
borderRight: '1px solid var(--editor-border, color-mix(in srgb, var(--theme-ink) 10%, transparent))',
|
|
72
|
+
fontFamily: 'inherit',
|
|
73
|
+
fontSize: 'inherit',
|
|
74
|
+
lineHeight: 'inherit',
|
|
75
|
+
},
|
|
76
|
+
'.cm-lineNumbers .cm-gutterElement': {
|
|
77
|
+
padding: '0 12px 0 16px',
|
|
78
|
+
minWidth: '1.5rem',
|
|
79
|
+
textAlign: 'right',
|
|
80
|
+
},
|
|
81
|
+
// The selection is drawn by `drawSelection()`; the native one is left to
|
|
82
|
+
// the browser when the editor is not focused.
|
|
83
|
+
'.cm-selectionBackground, &.cm-focused .cm-selectionBackground': {
|
|
84
|
+
backgroundColor: 'var(--editor-selection, color-mix(in srgb, var(--theme-ink) 20%, transparent))',
|
|
85
|
+
},
|
|
86
|
+
'.cm-cursor, .cm-dropCursor': {
|
|
87
|
+
borderLeftColor: 'var(--editor-cursor, var(--theme-ink))',
|
|
88
|
+
},
|
|
89
|
+
// A find match, painted by the decoration field (`marks.ts`). The geometry
|
|
90
|
+
// rules are the ones the old markup layer needed: horizontal edges only, or
|
|
91
|
+
// a match spanning several tokens paints a seam between them.
|
|
92
|
+
'.cm-content .find-match': {
|
|
93
|
+
color: 'inherit',
|
|
94
|
+
backgroundColor: 'var(--editor-find, color-mix(in srgb, var(--theme-warning) 32%, transparent))',
|
|
95
|
+
},
|
|
96
|
+
'.cm-content .find-match[data-find-current]': {
|
|
97
|
+
backgroundColor:
|
|
98
|
+
'var(--editor-find-current, color-mix(in srgb, var(--theme-warning) 55%, transparent))',
|
|
99
|
+
borderTop: '1px solid color-mix(in srgb, var(--theme-warning) 75%, transparent)',
|
|
100
|
+
borderBottom: '1px solid color-mix(in srgb, var(--theme-warning) 75%, transparent)',
|
|
101
|
+
},
|
|
102
|
+
// Shiki tokens. The colour itself comes from the custom properties the
|
|
103
|
+
// decoration carries; `tailwind.css` holds the same pair for every other
|
|
104
|
+
// surface, and this block is the editor's copy of it. The `&`-less form
|
|
105
|
+
// below is CM6's base-class prefix, and the second rule is why an app theme
|
|
106
|
+
// flip repaints tokens with no re-tokenize.
|
|
107
|
+
'.cm-content .shiki-tok': { color: 'var(--shiki-light)' },
|
|
108
|
+
'html[data-theme-variant="dark"] & .cm-content .shiki-tok': { color: 'var(--shiki-dark)' },
|
|
109
|
+
'.cm-placeholder': { color: 'color-mix(in srgb, var(--editor-fg, var(--theme-ink)) 35%, transparent)' },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The two width-mode buttons, driven through the REAL session-state provider.
|
|
8
|
+
*
|
|
9
|
+
* Three properties only a mount can see, and each fails silently:
|
|
10
|
+
*
|
|
11
|
+
* - The two toolbars draw the SAME state. `useSessionState` hands every caller
|
|
12
|
+
* a private copy, so a version reading it would leave the diff toolbar
|
|
13
|
+
* showing `normal` after the editor toolbar entered compact view — two
|
|
14
|
+
* buttons disagreeing about one layout.
|
|
15
|
+
* - Each button reports `aria-pressed` for its own mode and nothing else, so
|
|
16
|
+
* the pair reads as a mode selection rather than as two unrelated switches.
|
|
17
|
+
* - Clicking the ACTIVE mode returns to `normal`, which is the only way back to
|
|
18
|
+
* a layout with the chat on screen without hunting for the other button.
|
|
19
|
+
*
|
|
20
|
+
* Rendered with `h()` against happy-dom, over a stubbed `fetch` that answers
|
|
21
|
+
* the session-state blob (the provider loads it before anything is ready).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
25
|
+
import { Window } from 'happy-dom';
|
|
26
|
+
import { h, render } from 'preact';
|
|
27
|
+
import { act } from 'preact/test-utils';
|
|
28
|
+
import { EditorViewButtons } from '@/client/components/common/editor-view-buttons';
|
|
29
|
+
import { SessionStateProvider } from '@/client/components/common/session-state-provider/index';
|
|
30
|
+
import { forgetSession } from '@/shared/lib/workspace/session-state/store';
|
|
31
|
+
import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
32
|
+
|
|
33
|
+
const SESSION = 'new-editor-view-buttons';
|
|
34
|
+
/** The runner's own fetch, reached through `Bun` so a leaked stub cannot be mistaken for it. */
|
|
35
|
+
const nativeFetch = Bun.fetch;
|
|
36
|
+
|
|
37
|
+
let container: HTMLElement;
|
|
38
|
+
|
|
39
|
+
beforeAll(() => {
|
|
40
|
+
installDomGlobals(new Window({ url: 'http://localhost' }));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
if (container) {
|
|
45
|
+
render(null, container);
|
|
46
|
+
container.remove();
|
|
47
|
+
}
|
|
48
|
+
forgetSession(SESSION);
|
|
49
|
+
(globalThis as unknown as Record<string, unknown>).fetch = nativeFetch;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterAll(() => {
|
|
53
|
+
restoreDomGlobals();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Mount TWO button pairs under one provider — the editor toolbar's and the diff
|
|
58
|
+
* toolbar's — because "they share one slot" is the claim, and a single pair
|
|
59
|
+
* could pass with private copies.
|
|
60
|
+
*/
|
|
61
|
+
async function mount(stored: Record<string, unknown> = {}): Promise<void> {
|
|
62
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
63
|
+
target.fetch = async () => {
|
|
64
|
+
const json = (body: unknown) =>
|
|
65
|
+
new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
66
|
+
return json({ sessionId: SESSION, state: stored });
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
container = document.createElement('div');
|
|
70
|
+
document.body.appendChild(container);
|
|
71
|
+
await act(async () => {
|
|
72
|
+
render(
|
|
73
|
+
h(SessionStateProvider, {
|
|
74
|
+
sessionId: SESSION,
|
|
75
|
+
children: h(
|
|
76
|
+
'div',
|
|
77
|
+
null,
|
|
78
|
+
h('div', { id: 'editor-toolbar' }, h(EditorViewButtons, { size: 14 })),
|
|
79
|
+
h('div', { id: 'diff-toolbar' }, h(EditorViewButtons, { size: 13 })),
|
|
80
|
+
),
|
|
81
|
+
}),
|
|
82
|
+
container,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
// The provider's blob load settles across a tick.
|
|
86
|
+
for (let i = 0; i < 10; i += 1) await act(async () => {});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The named button inside one of the two toolbar hosts. */
|
|
90
|
+
function button(toolbar: 'editor-toolbar' | 'diff-toolbar', name: string): HTMLButtonElement {
|
|
91
|
+
const host = container.querySelector(`#${toolbar}`);
|
|
92
|
+
const found = [...(host?.querySelectorAll('button') ?? [])].find(
|
|
93
|
+
(el) => el.getAttribute('aria-label') === name,
|
|
94
|
+
);
|
|
95
|
+
if (!found) throw new Error(`no "${name}" button in #${toolbar}`);
|
|
96
|
+
return found as HTMLButtonElement;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function click(el: HTMLButtonElement): Promise<void> {
|
|
100
|
+
await act(async () => {
|
|
101
|
+
el.dispatchEvent(new window.MouseEvent('click', { bubbles: true }) as never);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe('EditorViewButtons', () => {
|
|
106
|
+
test('both toolbars offer both modes, and neither is pressed by default', async () => {
|
|
107
|
+
await mount();
|
|
108
|
+
|
|
109
|
+
for (const toolbar of ['editor-toolbar', 'diff-toolbar'] as const) {
|
|
110
|
+
expect(button(toolbar, 'Compact view').getAttribute('aria-pressed')).toBe('false');
|
|
111
|
+
expect(button(toolbar, 'Full view').getAttribute('aria-pressed')).toBe('false');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('a click enters the mode, and the OTHER toolbar shows the same state', async () => {
|
|
116
|
+
await mount();
|
|
117
|
+
await click(button('editor-toolbar', 'Compact view'));
|
|
118
|
+
|
|
119
|
+
expect(button('editor-toolbar', 'Compact view').getAttribute('aria-pressed')).toBe('true');
|
|
120
|
+
expect(button('diff-toolbar', 'Compact view').getAttribute('aria-pressed')).toBe('true');
|
|
121
|
+
expect(button('diff-toolbar', 'Full view').getAttribute('aria-pressed')).toBe('false');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('the two modes are exclusive: entering one clears the other', async () => {
|
|
125
|
+
await mount();
|
|
126
|
+
await click(button('editor-toolbar', 'Compact view'));
|
|
127
|
+
await click(button('diff-toolbar', 'Full view'));
|
|
128
|
+
|
|
129
|
+
expect(button('editor-toolbar', 'Compact view').getAttribute('aria-pressed')).toBe('false');
|
|
130
|
+
expect(button('editor-toolbar', 'Full view').getAttribute('aria-pressed')).toBe('true');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('clicking the active mode returns to the ordinary layout', async () => {
|
|
134
|
+
await mount();
|
|
135
|
+
await click(button('editor-toolbar', 'Full view'));
|
|
136
|
+
await click(button('editor-toolbar', 'Full view'));
|
|
137
|
+
|
|
138
|
+
expect(button('editor-toolbar', 'Full view').getAttribute('aria-pressed')).toBe('false');
|
|
139
|
+
expect(button('editor-toolbar', 'Compact view').getAttribute('aria-pressed')).toBe('false');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('a mode stored for this session is restored on mount', async () => {
|
|
143
|
+
await mount({ 'layout.editorViewMode': 'full' });
|
|
144
|
+
|
|
145
|
+
expect(button('editor-toolbar', 'Full view').getAttribute('aria-pressed')).toBe('true');
|
|
146
|
+
expect(button('diff-toolbar', 'Full view').getAttribute('aria-pressed')).toBe('true');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { MessageSquareOff, PanelLeftClose } from 'lucide-preact';
|
|
2
|
+
import type { LucideIcon } from 'lucide-preact';
|
|
3
|
+
import { useSharedSessionState } from '@/client/hooks/workspace/session-state';
|
|
4
|
+
import type { ToolbarOverflowAction } from '@/client/components/common/toolbar-row';
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_EDITOR_VIEW_MODE,
|
|
7
|
+
EDITOR_VIEW_MODE_KEY,
|
|
8
|
+
isEditorViewMode,
|
|
9
|
+
toggledEditorViewMode,
|
|
10
|
+
type EditorViewMode,
|
|
11
|
+
type EditorViewModeTarget,
|
|
12
|
+
} from '@/shared/lib/workspace/editor-view-mode';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The two width modes, as data.
|
|
16
|
+
*
|
|
17
|
+
* Each is a row so the buttons, their labels and their icons cannot drift
|
|
18
|
+
* apart: the label states exactly which columns disappear, because "Compact"
|
|
19
|
+
* and "Full" are words that could mean anything, and the icons are DIFFERENT
|
|
20
|
+
* SHAPES rather than two spellings of the same one — the third width control
|
|
21
|
+
* beside them (full SCREEN, which covers the right panel too) keeps its own
|
|
22
|
+
* `Maximize` glyph.
|
|
23
|
+
*/
|
|
24
|
+
export const EDITOR_VIEW_MODES: Array<{
|
|
25
|
+
target: EditorViewModeTarget;
|
|
26
|
+
label: string;
|
|
27
|
+
hint: string;
|
|
28
|
+
Icon: LucideIcon;
|
|
29
|
+
}> = [
|
|
30
|
+
{
|
|
31
|
+
target: 'compact',
|
|
32
|
+
label: 'Compact view',
|
|
33
|
+
hint: 'Editor takes the chat width — chat hidden, sidebar and right panel kept',
|
|
34
|
+
Icon: MessageSquareOff,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
target: 'full',
|
|
38
|
+
label: 'Full view',
|
|
39
|
+
hint: 'Editor takes the chat and sidebar width — chat and sidebar hidden, right panel kept',
|
|
40
|
+
Icon: PanelLeftClose,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** The mode in force, and the setter the buttons share. */
|
|
45
|
+
export function useEditorViewMode(): { mode: EditorViewMode; setMode: (next: EditorViewMode) => void } {
|
|
46
|
+
const [stored, setMode] = useSharedSessionState<EditorViewMode>(EDITOR_VIEW_MODE_KEY, DEFAULT_EDITOR_VIEW_MODE);
|
|
47
|
+
return { mode: isEditorViewMode(stored) ? stored : DEFAULT_EDITOR_VIEW_MODE, setMode };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The same two modes as `⋯` menu rows, for a toolbar too narrow to draw them.
|
|
52
|
+
*
|
|
53
|
+
* A hook rather than a plain function because the menu row has to CLICK the same
|
|
54
|
+
* slot the buttons write, and a row built from a stale mode would enter the mode
|
|
55
|
+
* already in force (which the toggle reads as "leave it").
|
|
56
|
+
*/
|
|
57
|
+
export function useEditorViewActions(): ToolbarOverflowAction[] {
|
|
58
|
+
const { mode, setMode } = useEditorViewMode();
|
|
59
|
+
return EDITOR_VIEW_MODES.map(({ target, label, Icon }) => ({
|
|
60
|
+
// A menu row has no hover title, so the sentence that explains the mode is
|
|
61
|
+
// part of what the row says rather than lost with the button's tooltip.
|
|
62
|
+
label,
|
|
63
|
+
icon: Icon,
|
|
64
|
+
active: mode === target,
|
|
65
|
+
onSelect: () => setMode(toggledEditorViewMode(mode, target)),
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface EditorViewButtonsProps {
|
|
70
|
+
/** Glyph size: the editor toolbar draws 14px, the diff toolbar 13px. */
|
|
71
|
+
size?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The two width-mode buttons, drawn by BOTH the editor and the diff toolbar.
|
|
76
|
+
*
|
|
77
|
+
* One component because the two toolbars are the same surface on two tab kinds:
|
|
78
|
+
* a mode entered from the diff and a mode entered from a file must be the same
|
|
79
|
+
* mode, and a second copy of these rules is how the diff ends up offering a
|
|
80
|
+
* width the layout does not implement. The state is the shared session slot
|
|
81
|
+
* (see `useEditorViewLayout`) so the layout collapses the columns on the same
|
|
82
|
+
* render.
|
|
83
|
+
*/
|
|
84
|
+
export function EditorViewButtons({ size = 14 }: EditorViewButtonsProps) {
|
|
85
|
+
const { mode, setMode } = useEditorViewMode();
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<>
|
|
89
|
+
{EDITOR_VIEW_MODES.map(({ target, label, hint, Icon }) => {
|
|
90
|
+
const active = mode === target;
|
|
91
|
+
return (
|
|
92
|
+
<button
|
|
93
|
+
key={target}
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={() => setMode(toggledEditorViewMode(mode, target))}
|
|
96
|
+
className={`flex items-center justify-center p-1.5 rounded transition-colors cursor-pointer flex-shrink-0 ${
|
|
97
|
+
active ? 'bg-ink/10 text-ink' : 'text-ink/50 hover:text-ink hover:bg-ink/5'
|
|
98
|
+
}`}
|
|
99
|
+
title={hint}
|
|
100
|
+
aria-label={label}
|
|
101
|
+
aria-pressed={active}
|
|
102
|
+
>
|
|
103
|
+
<Icon size={size} />
|
|
104
|
+
</button>
|
|
105
|
+
);
|
|
106
|
+
})}
|
|
107
|
+
</>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The shell update's dialog: confirm, install, report.
|
|
8
|
+
*
|
|
9
|
+
* ## Why the message lives HERE and not in the footer
|
|
10
|
+
*
|
|
11
|
+
* The footer is a row of controls, one of them 24px wide. A sentence like "Kabmo
|
|
12
|
+
* 0.3.2 installed — restarting" pushed into that row pushed the controls around
|
|
13
|
+
* and read as a status line for the SIDEBAR rather than for the update the user
|
|
14
|
+
* just asked for. The control therefore stays a control, and everything a run has
|
|
15
|
+
* to say — the confirmation before it, the progress during it, the shell's own
|
|
16
|
+
* refusal after it — is a dialog.
|
|
17
|
+
*
|
|
18
|
+
* ## The confirmation is not ceremony
|
|
19
|
+
*
|
|
20
|
+
* The click replaces the app the user is looking at and restarts it. That is not
|
|
21
|
+
* something a 24px glyph should do on a single press with no words, which is why
|
|
22
|
+
* the first thing the dialog says is what will happen and what will be lost
|
|
23
|
+
* (nothing but the window: the servers and the session are the console's own
|
|
24
|
+
* state, and they survive).
|
|
25
|
+
*
|
|
26
|
+
* ## Every phase reports the shell's own words
|
|
27
|
+
*
|
|
28
|
+
* `done` and `error` print the sentence the SHELL sent. Nothing here invents
|
|
29
|
+
* "Update failed": a shell that verified the download and declined knows why, and
|
|
30
|
+
* that reason is the only thing a user can act on.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { AlertCircle, Check, Download, Loader2 } from 'lucide-preact';
|
|
34
|
+
import { Modal } from '@/client/components/common/Modal';
|
|
35
|
+
import {
|
|
36
|
+
shellDownloadLabel,
|
|
37
|
+
type ShellDownload,
|
|
38
|
+
} from '@/client/hooks/ui/updates/shell-download';
|
|
39
|
+
import type { ShellUpdatePhase } from '@/client/hooks/ui/updates/shell-update';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What the dialog is showing.
|
|
43
|
+
*
|
|
44
|
+
* `confirm` is the dialog's own phase, not the hook's: the hook's `idle` means "no
|
|
45
|
+
* run has happened", and the dialog is only ever open on a run the user is about to
|
|
46
|
+
* start or one that already ran.
|
|
47
|
+
*/
|
|
48
|
+
export type ShellUpdateModalPhase = ShellUpdatePhase | 'confirm';
|
|
49
|
+
|
|
50
|
+
interface ShellUpdateModalProps {
|
|
51
|
+
download: ShellDownload;
|
|
52
|
+
phase: ShellUpdateModalPhase;
|
|
53
|
+
message: string | null;
|
|
54
|
+
onConfirm: () => void;
|
|
55
|
+
onClose: () => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** What this platform actually does with the artifact, in the platform's terms. */
|
|
59
|
+
function installNote(download: ShellDownload): string {
|
|
60
|
+
switch (download.platform) {
|
|
61
|
+
case 'macos':
|
|
62
|
+
return 'Kabmo downloads the new build, checks its signature and replaces itself. The window reopens on the new version.';
|
|
63
|
+
case 'windows':
|
|
64
|
+
return 'Kabmo downloads the new installer and runs it silently. The window closes and reopens on the new version.';
|
|
65
|
+
case 'linux':
|
|
66
|
+
return 'Kabmo downloads the new AppImage over the one it is running from and restarts. A package install (.deb / .rpm) cannot replace itself — that build is offered the release page instead.';
|
|
67
|
+
default:
|
|
68
|
+
return 'Kabmo installs the new build and restarts.';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const SECONDARY =
|
|
73
|
+
'rounded-md border border-ink/20 px-3 py-1.5 text-[11px] font-medium text-ink transition-colors hover:border-ink/40 cursor-pointer';
|
|
74
|
+
const PRIMARY =
|
|
75
|
+
'inline-flex items-center gap-1.5 rounded-md bg-ink px-3 py-1.5 text-[11px] font-medium text-paper transition-opacity hover:opacity-90 disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer';
|
|
76
|
+
|
|
77
|
+
export function ShellUpdateModal({ download, phase, message, onConfirm, onClose }: ShellUpdateModalProps) {
|
|
78
|
+
const label = shellDownloadLabel(download);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Modal
|
|
82
|
+
onClose={onClose}
|
|
83
|
+
maxWidthClass="max-w-sm"
|
|
84
|
+
header={<h2 className="text-sm font-semibold text-ink">{label}</h2>}
|
|
85
|
+
footer={
|
|
86
|
+
<div className="flex items-center justify-end gap-2 px-5 pb-4">
|
|
87
|
+
{phase === 'confirm' && (
|
|
88
|
+
<>
|
|
89
|
+
<button type="button" className={SECONDARY} onClick={onClose}>
|
|
90
|
+
Cancel
|
|
91
|
+
</button>
|
|
92
|
+
<button type="button" className={PRIMARY} onClick={onConfirm}>
|
|
93
|
+
<Download size={13} />
|
|
94
|
+
<span>Install update</span>
|
|
95
|
+
</button>
|
|
96
|
+
</>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{phase === 'working' && (
|
|
100
|
+
<button type="button" className={PRIMARY} disabled>
|
|
101
|
+
<Loader2 size={13} className="animate-spin motion-reduce:animate-none" />
|
|
102
|
+
<span>Installing…</span>
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{phase === 'done' && (
|
|
107
|
+
<button type="button" className={PRIMARY} onClick={onClose}>
|
|
108
|
+
<Check size={13} />
|
|
109
|
+
<span>Close</span>
|
|
110
|
+
</button>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{phase === 'error' && (
|
|
114
|
+
<>
|
|
115
|
+
<button type="button" className={SECONDARY} onClick={onClose}>
|
|
116
|
+
Close
|
|
117
|
+
</button>
|
|
118
|
+
{/* Retry re-runs the SAME offer: the release is still the one the
|
|
119
|
+
console read, and a failure here is the shell's download or its
|
|
120
|
+
verification, not a stale version. */}
|
|
121
|
+
<button type="button" className={PRIMARY} onClick={onConfirm}>
|
|
122
|
+
<Download size={13} />
|
|
123
|
+
<span>Try again</span>
|
|
124
|
+
</button>
|
|
125
|
+
</>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
}
|
|
129
|
+
>
|
|
130
|
+
<div className="px-5 py-4 space-y-3 text-[11px] leading-5 text-ink/70">
|
|
131
|
+
{phase === 'confirm' && (
|
|
132
|
+
<>
|
|
133
|
+
<p>{installNote(download)}</p>
|
|
134
|
+
<p className="text-ink/50">
|
|
135
|
+
Version {download.latest} · the console keeps running; only this window restarts.
|
|
136
|
+
</p>
|
|
137
|
+
</>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{phase === 'working' && (
|
|
141
|
+
<p className="flex items-center gap-2 text-ink">
|
|
142
|
+
<Loader2 size={14} className="animate-spin motion-reduce:animate-none" />
|
|
143
|
+
<span>Downloading and verifying the new build…</span>
|
|
144
|
+
</p>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{(phase === 'done' || phase === 'error') && (
|
|
148
|
+
<p
|
|
149
|
+
role="status"
|
|
150
|
+
className={`flex items-start gap-2 ${phase === 'error' ? 'text-error' : 'text-ink'}`}
|
|
151
|
+
>
|
|
152
|
+
{phase === 'error' ? (
|
|
153
|
+
<AlertCircle size={14} className="mt-0.5 shrink-0" />
|
|
154
|
+
) : (
|
|
155
|
+
<Check size={14} className="mt-0.5 shrink-0" />
|
|
156
|
+
)}
|
|
157
|
+
<span>{message ?? (phase === 'error' ? 'The update failed.' : 'Update installed.')}</span>
|
|
158
|
+
</p>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</Modal>
|
|
162
|
+
);
|
|
163
|
+
}
|