ompchamber 3.0.0 → 3.1.1
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 +30 -8
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +333 -333
- package/dist/client/static/js/index-5yfre1kw.css +1 -0
- package/dist/client/static/js/{index-prbmx3yj.js → index-qhzsybkn.js} +643 -617
- package/package.json +1 -1
- package/src/cli/lib/args.js +4 -3
- package/src/cli/lib/commands/restart.js +7 -6
- package/src/cli/lib/commands/stop.js +38 -31
- package/src/cli/lib/commands/update.js +3 -2
- package/src/cli/lib/stop-scope.js +23 -0
- package/src/cli/lib/stop-scope.test.js +41 -0
- package/src/client/components/common/code-editor/handler.test.ts +274 -0
- package/src/client/components/common/code-editor/handler.ts +175 -0
- package/src/client/components/common/code-editor/index.tsx +185 -220
- package/src/client/components/common/code-surface/index.tsx +109 -12
- package/src/client/components/common/lazy-panels.tsx +5 -1
- package/src/client/components/common/viewport.ts +5 -3
- package/src/client/components/layout/RightActivityBar.tsx +2 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
- package/src/client/components/layout/desktop-layout/index.tsx +24 -4
- package/src/client/components/mobile/RightSidebar.tsx +11 -4
- package/src/client/components/workspace/browser-panel/AddressBar.tsx +1 -10
- package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
- package/src/client/components/workspace/browser-panel/index.tsx +6 -6
- package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
- package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.tsx +1 -1
- package/src/client/components/workspace/editor/CommandPalette.test.ts +242 -0
- package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
- package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
- package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
- package/src/client/components/workspace/editor/Toolbar.tsx +19 -1
- package/src/client/components/workspace/editor/index.tsx +153 -53
- package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
- package/src/client/components/workspace/todo-panel/PhaseGroup.tsx +76 -0
- package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
- package/src/client/components/workspace/todo-panel/index.tsx +161 -0
- package/src/client/hooks/editor/find-harness.test-util.ts +170 -0
- package/src/client/hooks/editor/scroll-container.test.ts +102 -0
- package/src/client/hooks/editor/scroll-container.ts +26 -0
- package/src/client/hooks/editor/use-code-window.ts +2 -20
- package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
- package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
- package/src/client/hooks/editor/use-editor-find.ts +321 -0
- package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
- package/src/client/hooks/editor/use-editor-history.ts +134 -0
- package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
- package/src/client/hooks/editor/use-occurrences.ts +98 -0
- package/src/client/hooks/workspace/file-tree-actions.test.ts +348 -0
- package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
- package/src/client/hooks/workspace/session-todos.ts +102 -0
- package/src/client/tailwind.css +30 -0
- package/src/server/lib/bundler/css.test.ts +87 -0
- package/src/server/lib/bundler/css.ts +17 -2
- package/src/server/lib/bundler/nested-at-rules.test.ts +100 -0
- package/src/server/lib/bundler/nested-at-rules.ts +146 -0
- package/src/server/lib/lifecycle/launch-mode.test.ts +20 -1
- package/src/server/lib/lifecycle/launch-mode.ts +26 -0
- package/src/server/lib/lifecycle/restart.ts +9 -14
- package/src/server/lib/omp/session/todos.ts +100 -0
- package/src/server/routes/fs/action-create.test.ts +90 -0
- package/src/server/routes/fs/action.ts +23 -0
- package/src/server/routes/omp/index.ts +2 -0
- package/src/server/routes/omp/session-todos.ts +80 -0
- package/src/shared/lib/chat/{todo-parser.ts → todo/parser.ts} +17 -0
- package/src/shared/lib/chat/todo/progress.ts +97 -0
- package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
- package/src/shared/lib/chat/todo/snapshot.ts +215 -0
- package/src/shared/lib/code/editor/commands.test.ts +155 -0
- package/src/shared/lib/code/editor/commands.ts +260 -0
- package/src/shared/lib/code/editor/edits.test.ts +100 -0
- package/src/shared/lib/code/editor/edits.ts +122 -0
- package/src/shared/lib/code/editor/find-status.test.ts +50 -0
- package/src/shared/lib/code/editor/find-status.ts +36 -0
- package/src/shared/lib/code/editor/find.test.ts +203 -0
- package/src/shared/lib/code/editor/find.ts +224 -0
- package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
- package/src/shared/lib/code/editor/keymap.test.ts +109 -0
- package/src/shared/lib/code/editor/keymap.ts +151 -0
- package/src/shared/lib/code/editor/layers.test.ts +77 -0
- package/src/shared/lib/code/editor/layers.ts +67 -0
- package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
- package/src/shared/lib/code/editor/line-edits.ts +159 -0
- package/src/shared/lib/code/editor/lines.test.ts +81 -0
- package/src/shared/lib/code/editor/lines.ts +83 -0
- package/src/shared/lib/code/editor/marks.test.ts +86 -0
- package/src/shared/lib/code/editor/marks.ts +106 -0
- package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
- package/src/shared/lib/code/editor/occurrences.ts +111 -0
- package/src/shared/lib/code/editor/palette.test.ts +91 -0
- package/src/shared/lib/code/editor/palette.ts +92 -0
- package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
- package/src/shared/lib/code/editor/reset-css.ts +32 -0
- package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
- package/src/shared/lib/code/editor/text-commands.ts +154 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
- package/src/shared/lib/code/syntax-highlight.ts +29 -17
- package/src/shared/lib/code/windowed-highlight.ts +52 -1
- package/src/shared/lib/ui/key-binding.test.ts +115 -0
- package/src/shared/lib/ui/key-binding.ts +143 -0
- package/src/shared/lib/util/platform.test.ts +87 -0
- package/src/shared/lib/util/platform.ts +30 -0
- package/src/shared/lib/workspace/keymap.test.ts +82 -0
- package/src/shared/lib/workspace/keymap.ts +30 -0
- package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
- package/src/shared/lib/workspace/right-panels.ts +8 -0
- package/src/shared/lib/workspace/session-state/store.ts +0 -1
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/todo.ts +108 -0
- package/dist/client/static/js/index-ranztes9.css +0 -1
- package/src/server/lib/lifecycle/restart.test.ts +0 -21
- /package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
1
|
+
import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
2
|
+
import { forwardRef } from 'preact/compat';
|
|
2
3
|
import type { CSSProperties } from 'preact';
|
|
3
4
|
|
|
4
|
-
import { CodeEditor } from '@/client/components/common/code-editor';
|
|
5
|
+
import { CodeEditor, type CodeEditorHandle } from '@/client/components/common/code-editor';
|
|
5
6
|
import { measureRowHeight, measureWrappedLines, type WrappedLines } from '@/client/components/common/code-surface/measure';
|
|
7
|
+
import { findScrollContainer } from '@/client/hooks/editor/scroll-container';
|
|
6
8
|
import { useCodeWindow } from '@/client/hooks/editor/use-code-window';
|
|
7
9
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
8
10
|
import {
|
|
@@ -12,17 +14,32 @@ import {
|
|
|
12
14
|
type LineGeometry,
|
|
13
15
|
type LineWindow,
|
|
14
16
|
} from '@/shared/lib/code/lazy-window';
|
|
17
|
+
import type { FindMatch } from '@/shared/lib/code/editor/find';
|
|
18
|
+
import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
|
|
19
|
+
import { lineIndexAt } from '@/shared/lib/code/editor/lines';
|
|
20
|
+
import type { TextRange } from '@/shared/lib/code/editor/commands';
|
|
21
|
+
import { countLines } from '@/shared/lib/code/editor/lines';
|
|
15
22
|
import { highlightCode } from '@/shared/lib/code/syntax-highlight';
|
|
16
23
|
import { highlightCodeWindow } from '@/shared/lib/code/windowed-highlight';
|
|
17
24
|
|
|
18
25
|
const WRAP_ON = '!whitespace-pre-wrap !break-words';
|
|
19
26
|
const WRAP_OFF = '!whitespace-pre !break-normal';
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
export interface CodeSurfaceHandle {
|
|
29
|
+
/** Current selection offsets in the buffer, or null before the field mounts. */
|
|
30
|
+
getSelection(): { start: number; end: number } | null;
|
|
31
|
+
/** Select `[start, end)`; `focus` moves keyboard focus into the editor. */
|
|
32
|
+
select(start: number, end: number, focus?: boolean): void;
|
|
33
|
+
/** Replace the buffer as one undoable edit, caret at `[caretStart, caretEnd)`, focus unchanged. */
|
|
34
|
+
applyDocument(value: string, caretStart: number, caretEnd: number): void;
|
|
35
|
+
/** Run an editor command by name (the palette's path into the editor). */
|
|
36
|
+
runCommand(command: EditorCommand): void;
|
|
37
|
+
/**
|
|
38
|
+
* Bring the line holding `offset` into view WITHOUT moving the caret — what
|
|
39
|
+
* "step to the next match" needs: the user's focus stays in the find field
|
|
40
|
+
* while the match scrolls under it.
|
|
41
|
+
*/
|
|
42
|
+
revealOffset(offset: number): void;
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
interface CodeSurfaceProps {
|
|
@@ -31,6 +48,19 @@ interface CodeSurfaceProps {
|
|
|
31
48
|
/** Shiki language id, already resolved from the file name. */
|
|
32
49
|
language: string;
|
|
33
50
|
wordWrap: boolean;
|
|
51
|
+
/** Find matches (document offsets) to paint; the current one is marked. */
|
|
52
|
+
marks?: readonly FindMatch[];
|
|
53
|
+
currentMark?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Extra ranges the ⌘D workflow has selected. The textarea can only hold one
|
|
56
|
+
* selection, so the surface paints the others from here — and it must, or
|
|
57
|
+
* ⌘D would look like it did nothing.
|
|
58
|
+
*/
|
|
59
|
+
occurrences?: readonly TextRange[];
|
|
60
|
+
/** Commands the editor does not own (find bar chords, word wrap, save). */
|
|
61
|
+
onCommand?: (command: EditorCommand) => void;
|
|
62
|
+
/** Called with the extra ranges whenever ⌘D / ⌘⇧L change them. */
|
|
63
|
+
onOccurrencesChange?: (ranges: readonly TextRange[]) => void;
|
|
34
64
|
/** Outer flex row that holds the gutter and the editor column. */
|
|
35
65
|
rootClassName?: string;
|
|
36
66
|
gutterClassName?: string;
|
|
@@ -71,11 +101,16 @@ function sameWrappedLines(previous: WrappedLines | null, next: WrappedLines | nu
|
|
|
71
101
|
* 20 000-line file from spending minutes in the tokenizer, and a keystroke in it
|
|
72
102
|
* from re-tokenizing the file at all.
|
|
73
103
|
*/
|
|
74
|
-
export function CodeSurface({
|
|
104
|
+
export const CodeSurface = forwardRef<CodeSurfaceHandle, CodeSurfaceProps>(function CodeSurface({
|
|
75
105
|
value,
|
|
76
106
|
onValueChange,
|
|
77
107
|
language,
|
|
78
108
|
wordWrap,
|
|
109
|
+
marks,
|
|
110
|
+
currentMark,
|
|
111
|
+
occurrences,
|
|
112
|
+
onCommand,
|
|
113
|
+
onOccurrencesChange,
|
|
79
114
|
rootClassName,
|
|
80
115
|
gutterClassName,
|
|
81
116
|
gutterStyle,
|
|
@@ -84,10 +119,11 @@ export function CodeSurface({
|
|
|
84
119
|
editorClassName,
|
|
85
120
|
editorPadding = 0,
|
|
86
121
|
editorStyle,
|
|
87
|
-
}
|
|
122
|
+
}, ref) {
|
|
88
123
|
const wrapClass = wordWrap ? WRAP_ON : WRAP_OFF;
|
|
89
124
|
const preRef = useRef<HTMLPreElement | null>(null);
|
|
90
125
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
126
|
+
const editorRef = useRef<CodeEditorHandle | null>(null);
|
|
91
127
|
const [wrapped, setWrapped] = useState<WrappedLines | null>(null);
|
|
92
128
|
const [rowHeight, setRowHeight] = useState<number | null>(null);
|
|
93
129
|
/** Lines the last measurement pass described; -1 before the first one runs. */
|
|
@@ -150,10 +186,66 @@ export function CodeSurface({
|
|
|
150
186
|
enabled: lazy && (measuredLines !== lineCount || geometry !== null),
|
|
151
187
|
});
|
|
152
188
|
|
|
189
|
+
/**
|
|
190
|
+
* What the highlight layer paints: the find matches, plus every extra ⌘D
|
|
191
|
+
* range. The find widget's current match is the one the caret is aimed at, so
|
|
192
|
+
* it keeps `currentMark` — an occurrence range never claims that role.
|
|
193
|
+
*/
|
|
194
|
+
const painted = useMemo<readonly FindMatch[]>(() => {
|
|
195
|
+
if (!occurrences || occurrences.length === 0) return marks ?? [];
|
|
196
|
+
return [...(marks ?? []), ...occurrences];
|
|
197
|
+
}, [marks, occurrences]);
|
|
198
|
+
/** Where the extra selections start inside `painted`; -1 when there are none. */
|
|
199
|
+
const occurrenceFrom = occurrences && occurrences.length > 0 ? (marks?.length ?? 0) : -1;
|
|
200
|
+
|
|
153
201
|
const highlight = useCallback(
|
|
154
202
|
(code: string, range: LineWindow | null) =>
|
|
155
|
-
range
|
|
156
|
-
|
|
203
|
+
range
|
|
204
|
+
? highlightCodeWindow(code, language, range, { marks: painted, currentMark, occurrenceFrom })
|
|
205
|
+
: highlightCode(code, language, { marks: painted, currentMark, occurrenceFrom }),
|
|
206
|
+
[language, painted, currentMark, occurrenceFrom],
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Bring `offset`'s row into view without touching the caret.
|
|
211
|
+
*
|
|
212
|
+
* `scrollIntoView` cannot be used: the caret's row and the match's row are
|
|
213
|
+
* different elements (the highlighted `<pre>` is `aria-hidden` and
|
|
214
|
+
* `pointer-events: none`), and the browser's own scroll-into-view on
|
|
215
|
+
* `setSelectionRange` would fight this. Instead the row's pixel offset comes
|
|
216
|
+
* from the same geometry the window is resolved against, so the two agree by
|
|
217
|
+
* construction, and the scroll is a plain assignment on the container the
|
|
218
|
+
* window reads.
|
|
219
|
+
*/
|
|
220
|
+
const revealOffset = useCallback(
|
|
221
|
+
(offset: number) => {
|
|
222
|
+
const root = rootRef.current;
|
|
223
|
+
if (!root || !geometry) return;
|
|
224
|
+
const rowTop = geometry.offsetOf(lineIndexAt(value, offset));
|
|
225
|
+
const container = findScrollContainer(root);
|
|
226
|
+
if (!container) return;
|
|
227
|
+
const frameTop = root.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
|
|
228
|
+
const band = container.clientHeight;
|
|
229
|
+
const target = frameTop + rowTop;
|
|
230
|
+
// Only scroll when the row is actually outside the band; a reveal that
|
|
231
|
+
// always re-centers would make every "next match" jump the whole view.
|
|
232
|
+
if (target < container.scrollTop + 8 || target + geometry.rowHeight > container.scrollTop + band - 8) {
|
|
233
|
+
container.scrollTop = Math.max(0, target - band / 3);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
[geometry, value],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
useImperativeHandle(
|
|
240
|
+
ref,
|
|
241
|
+
() => ({
|
|
242
|
+
getSelection: () => editorRef.current?.getSelection() ?? null,
|
|
243
|
+
select: (start, end, focus = true) => editorRef.current?.select(start, end, focus),
|
|
244
|
+
applyDocument: (next, caretStart, caretEnd) => editorRef.current?.applyDocument(next, caretStart, caretEnd),
|
|
245
|
+
runCommand: (command) => editorRef.current?.run(command),
|
|
246
|
+
revealOffset,
|
|
247
|
+
}),
|
|
248
|
+
[revealOffset],
|
|
157
249
|
);
|
|
158
250
|
|
|
159
251
|
const firstLine = virtual ? virtual.start : 0;
|
|
@@ -184,13 +276,18 @@ export function CodeSurface({
|
|
|
184
276
|
highlight={highlight}
|
|
185
277
|
padding={editorPadding}
|
|
186
278
|
preRef={preRef}
|
|
279
|
+
handleRef={editorRef}
|
|
187
280
|
textareaClassName={`focus:outline-none ${wrapClass}`}
|
|
188
281
|
preClassName={wrapClass}
|
|
189
282
|
style={editorStyle}
|
|
190
283
|
className={editorClassName}
|
|
191
284
|
virtual={virtual}
|
|
285
|
+
language={language}
|
|
286
|
+
occurrences={occurrences}
|
|
287
|
+
onCommand={onCommand}
|
|
288
|
+
onOccurrencesChange={onOccurrencesChange}
|
|
192
289
|
/>
|
|
193
290
|
</div>
|
|
194
291
|
</div>
|
|
195
292
|
);
|
|
196
|
-
}
|
|
293
|
+
});
|
|
@@ -32,6 +32,9 @@ export const LazyUserBrowserPanel = lazy(() =>
|
|
|
32
32
|
export const LazyUsagePanel = lazy(() =>
|
|
33
33
|
import('@/client/components/workspace/usage-panel/index').then((m) => ({ default: m.UsagePanel }))
|
|
34
34
|
);
|
|
35
|
+
export const LazyTodoPanel = lazy(() =>
|
|
36
|
+
import('@/client/components/workspace/todo-panel/index').then((m) => ({ default: m.TodoPanel }))
|
|
37
|
+
);
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
40
|
* One cached lazy per right-panel view for the desktop layout.
|
|
@@ -67,7 +70,8 @@ export function getDesktopPanelView(scope: object, view: RightPanelType): Compon
|
|
|
67
70
|
: view === 'context' ? LazyContextPanel
|
|
68
71
|
: view === 'user-browser' ? LazyUserBrowserPanel
|
|
69
72
|
: view === 'browser' ? LazyBrowserPanel
|
|
70
|
-
: LazyUsagePanel
|
|
73
|
+
: view === 'usage' ? LazyUsagePanel
|
|
74
|
+
: LazyTodoPanel;
|
|
71
75
|
const Cached: ComponentType<Record<string, unknown>> = ({ className, ...rest }) => (
|
|
72
76
|
<div className={`w-full h-full ${typeof className === 'string' ? className : ''}`}>
|
|
73
77
|
<Source {...rest} className="w-full h-full" />
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Device-viewport presets
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
7
|
+
* Device-viewport presets for the USER browser panel's iframe. There the box
|
|
8
|
+
* *is* the layout viewport, so a preset re-runs the page's media queries for
|
|
9
|
+
* real. The AGENT panel does not use these: its surface is a fixed-resolution
|
|
10
|
+
* screencast JPEG, which a preset could only letterbox while claiming to
|
|
11
|
+
* emulate a device (emulation belongs to the agent: `tab.emulate()`).
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import type { ViewportMode } from '@/shared/types';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BarChart3, Bot, Files, GitBranch, Globe, Layers, Search, Terminal } from 'lucide-preact';
|
|
1
|
+
import { BarChart3, Bot, Files, GitBranch, Globe, Layers, ListTodo, Search, Terminal } from 'lucide-preact';
|
|
2
2
|
import type { ReactNode } from 'preact/compat';
|
|
3
3
|
import { useGitStatus } from '@/client/hooks/workspace/git-status';
|
|
4
4
|
import { GIT_STATUS_POLL_MS } from '@/shared/lib/workspace/refresh-cadence';
|
|
@@ -27,6 +27,7 @@ const PANEL_META: Record<RightPanelType, { title: string; icon: ReactNode }> = {
|
|
|
27
27
|
'user-browser': { title: 'Browser (Anda)', icon: <Globe size={16} /> },
|
|
28
28
|
browser: { title: 'Browser Agent', icon: <Bot size={16} /> },
|
|
29
29
|
usage: { title: 'Usage', icon: <BarChart3 size={16} /> },
|
|
30
|
+
todo: { title: 'Todos', icon: <ListTodo size={16} /> },
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
export function RightActivityBar({ activePanel, onChangePanel, isPanelOpen, hasActiveContext, activeProjectPath, refreshKey }: RightActivityBarProps) {
|
|
@@ -2,6 +2,19 @@ import { LayoutTemplate, MoreHorizontal, PanelLeft, PanelRight, PanelRightClose,
|
|
|
2
2
|
import { PWAInstallButton } from '@/client/components/common/PWAInstallButton';
|
|
3
3
|
import { StreamStatusDot } from '@/client/components/common/StreamStatusDot';
|
|
4
4
|
import type { AgentStreamStatus } from '@/shared/lib/chat/omp/status';
|
|
5
|
+
import { WORKSPACE_KEY_BINDINGS, type WorkspaceCommand } from '@/shared/lib/workspace/keymap';
|
|
6
|
+
import { describeBinding } from '@/shared/lib/ui/key-binding';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A tooltip's text with the chord that actually invokes it. Derived from the
|
|
10
|
+
* workspace keymap rather than typed out, because a hand-written "⌘B" beside a
|
|
11
|
+
* binding that listens for something else is exactly the drift the keymap
|
|
12
|
+
* table exists to prevent.
|
|
13
|
+
*/
|
|
14
|
+
function withChord(command: WorkspaceCommand, label: string): string {
|
|
15
|
+
const binding = WORKSPACE_KEY_BINDINGS.find((entry) => entry.command === command);
|
|
16
|
+
return binding ? `${label} (${describeBinding(binding)})` : label;
|
|
17
|
+
}
|
|
5
18
|
|
|
6
19
|
interface TopNavbarProps {
|
|
7
20
|
sessionTitle: string | null;
|
|
@@ -42,8 +55,8 @@ export function TopNavbar({
|
|
|
42
55
|
type="button"
|
|
43
56
|
onClick={onToggleLeftPanel}
|
|
44
57
|
className="p-1.5 rounded hover:bg-ink/10 transition-colors text-ink/60 hover:text-ink cursor-pointer flex-shrink-0 titlebar-no-drag"
|
|
45
|
-
title=
|
|
46
|
-
aria-label=
|
|
58
|
+
title={withChord('toggleSidebar', 'Expand Sidebar')}
|
|
59
|
+
aria-label={withChord('toggleSidebar', 'Expand Sidebar')}
|
|
47
60
|
>
|
|
48
61
|
<PanelLeft size={16} />
|
|
49
62
|
</button>
|
|
@@ -86,7 +99,9 @@ export function TopNavbar({
|
|
|
86
99
|
type="button"
|
|
87
100
|
onClick={onToggleEditor}
|
|
88
101
|
className={`p-1.5 rounded hover:bg-ink/10 transition-colors cursor-pointer ${showEditor ? 'text-ink' : 'text-ink/40'}`}
|
|
89
|
-
title=
|
|
102
|
+
title={withChord('toggleEditorPanel', 'Toggle Editor Layout')}
|
|
103
|
+
aria-label={withChord('toggleEditorPanel', 'Toggle Editor Layout')}
|
|
104
|
+
aria-pressed={showEditor}
|
|
90
105
|
>
|
|
91
106
|
<LayoutTemplate size={16} />
|
|
92
107
|
</button>
|
|
@@ -94,7 +109,9 @@ export function TopNavbar({
|
|
|
94
109
|
type="button"
|
|
95
110
|
onClick={onToggleRightPanel}
|
|
96
111
|
className={`p-1.5 rounded hover:bg-ink/10 transition-colors cursor-pointer ${showRightPanel ? 'text-ink' : 'text-ink/40'}`}
|
|
97
|
-
title=
|
|
112
|
+
title={withChord('toggleRightPanel', 'Toggle Right Panel')}
|
|
113
|
+
aria-label={withChord('toggleRightPanel', 'Toggle Right Panel')}
|
|
114
|
+
aria-pressed={showRightPanel}
|
|
98
115
|
>
|
|
99
116
|
{showRightPanel ? <PanelRightClose size={16} /> : <PanelRight size={16} />}
|
|
100
117
|
</button>
|
|
@@ -16,6 +16,8 @@ import { useSidebarData } from '@/client/hooks/chat/omp/session-list';
|
|
|
16
16
|
import { DEFAULT_LEFT_PANEL_WIDTH, MAX_LEFT_PANEL_WIDTH, MIN_LEFT_PANEL_WIDTH, type EditorWidthMode } from '@/shared/lib/workspace/panel-widths';
|
|
17
17
|
import { ResizeHandle } from '@/client/components/layout/desktop-layout/ResizeHandle';
|
|
18
18
|
import { useChamberEvent, useWindowEvent } from '@/client/hooks/ui/window-event';
|
|
19
|
+
import { WORKSPACE_KEY_BINDINGS } from '@/shared/lib/workspace/keymap';
|
|
20
|
+
import { resolveBinding } from '@/shared/lib/ui/key-binding';
|
|
19
21
|
|
|
20
22
|
interface DesktopLayoutProps {
|
|
21
23
|
sessionId: string | null;
|
|
@@ -84,13 +86,31 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
84
86
|
const [sessionTitle, setSessionTitle] = useState<string | null>(null);
|
|
85
87
|
const streamStatus = useAgentStreamStatus();
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The workspace's own shortcuts, resolved from the shared keymap so the
|
|
91
|
+
* tooltips on the panel toggles name the chord that is actually bound.
|
|
92
|
+
*
|
|
93
|
+
* `⌘S` is deliberately absent here: the editor panel already owns it (it is
|
|
94
|
+
* the only surface with a buffer to save), and binding it twice would save
|
|
95
|
+
* the file twice.
|
|
96
|
+
*/
|
|
88
97
|
useWindowEvent('keydown', (e) => {
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
const command = resolveBinding(WORKSPACE_KEY_BINDINGS, e);
|
|
99
|
+
if (!command) return;
|
|
100
|
+
if (command === 'saveFile') return;
|
|
101
|
+
if (command === 'toggleEditorPanel' && !showEditor) {
|
|
102
|
+
// The editor panel is only meaningful with a file open, and `showEditor`
|
|
103
|
+
// already folds in the user's own toggle.
|
|
104
|
+
if (openedFiles.length === 0) return;
|
|
105
|
+
}
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
if (command === 'toggleSidebar') setShowLeftPanel((previous) => !previous);
|
|
108
|
+
else if (command === 'toggleEditorPanel') setUserToggledEditor((previous) => !(previous ?? showEditor));
|
|
109
|
+
else if (command === 'toggleRightPanel') setShowRightPanel((previous) => !previous);
|
|
110
|
+
else if (command === 'openSettings') {
|
|
91
111
|
setSettingsCategory('appearance');
|
|
92
112
|
setAutoOpenAddProvider(false);
|
|
93
|
-
setSettingsOpen(
|
|
113
|
+
setSettingsOpen((previous) => !previous);
|
|
94
114
|
}
|
|
95
115
|
});
|
|
96
116
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useState } from 'preact/hooks';
|
|
2
2
|
import { Suspense } from 'preact/compat';
|
|
3
3
|
import type { ReactNode } from 'preact/compat';
|
|
4
|
-
import { BarChart3, Bot, Files, GitBranch, Globe, Layers, Search, Terminal, X } from 'lucide-preact';
|
|
5
|
-
import { LazyBrowserPanel, LazyContextPanel, LazyFileExplorer, LazyGitPanel, LazySearchPanel, LazyTerminalPanel, LazyUsagePanel, LazyUserBrowserPanel } from '@/client/components/common/lazy-panels';
|
|
4
|
+
import { BarChart3, Bot, Files, GitBranch, Globe, Layers, ListTodo, Search, Terminal, X } from 'lucide-preact';
|
|
5
|
+
import { LazyBrowserPanel, LazyContextPanel, LazyFileExplorer, LazyGitPanel, LazySearchPanel, LazyTerminalPanel, LazyTodoPanel, LazyUsagePanel, LazyUserBrowserPanel } from '@/client/components/common/lazy-panels';
|
|
6
6
|
import { useGitStatus } from '@/client/hooks/workspace/git-status';
|
|
7
7
|
import { GIT_STATUS_POLL_MS } from '@/shared/lib/workspace/refresh-cadence';
|
|
8
8
|
import { RIGHT_PANEL_TYPES, type RightPanelType } from '@/shared/lib/workspace/right-panels';
|
|
@@ -31,6 +31,7 @@ const PANEL_META: Record<RightPanelType, { title: string; label: string; icon: R
|
|
|
31
31
|
'user-browser': { title: 'Browser (Anda)', label: 'Browser', icon: <Globe size={14} className="flex-shrink-0" /> },
|
|
32
32
|
browser: { title: 'Browser Agent', label: 'Agent', icon: <Bot size={14} className="flex-shrink-0" /> },
|
|
33
33
|
usage: { title: 'Usage', label: 'Usage', icon: <BarChart3 size={14} className="flex-shrink-0" /> },
|
|
34
|
+
todo: { title: 'Todos', label: 'Todos', icon: <ListTodo size={14} className="flex-shrink-0" /> },
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
export function MobileRightSidebar({
|
|
@@ -100,12 +101,17 @@ export function MobileRightSidebar({
|
|
|
100
101
|
</button>
|
|
101
102
|
</div>
|
|
102
103
|
|
|
103
|
-
{/* Main Tab Content — shared components so mobile == desktop features
|
|
104
|
+
{/* Main Tab Content — shared components so mobile == desktop features.
|
|
105
|
+
The workspace gate below applies per TAB, not to the whole drawer: a
|
|
106
|
+
todo list belongs to the session, not to the folder its cwd resolves
|
|
107
|
+
to, so the Todos tab stays reachable for a session running outside
|
|
108
|
+
every registered workspace. Every other view here reads the working
|
|
109
|
+
tree and genuinely needs one. */}
|
|
104
110
|
<div
|
|
105
111
|
className="flex-1 min-h-0 overflow-hidden relative"
|
|
106
112
|
style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }}
|
|
107
113
|
>
|
|
108
|
-
{!enabled ? (
|
|
114
|
+
{!enabled && activeTab !== 'todo' ? (
|
|
109
115
|
<div className="h-full flex items-center justify-center text-ink/40">
|
|
110
116
|
<span className="text-xs font-mono">No session selected</span>
|
|
111
117
|
</div>
|
|
@@ -130,6 +136,7 @@ export function MobileRightSidebar({
|
|
|
130
136
|
{activeTab === 'user-browser' && <LazyUserBrowserPanel className="h-full w-full" />}
|
|
131
137
|
{activeTab === 'browser' && <LazyBrowserPanel className="h-full w-full" active />}
|
|
132
138
|
{activeTab === 'usage' && <LazyUsagePanel className="h-full w-full" />}
|
|
139
|
+
{activeTab === 'todo' && <LazyTodoPanel className="h-full w-full" />}
|
|
133
140
|
</Suspense>
|
|
134
141
|
</>
|
|
135
142
|
)}
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { Globe, RotateCw } from 'lucide-preact';
|
|
2
2
|
import { BrowserToolbar, ICON_BUTTON } from '@/client/components/workspace/browser-toolbar/index';
|
|
3
|
-
import type { BrowserViewStatus
|
|
3
|
+
import type { BrowserViewStatus } from '@/shared/types';
|
|
4
4
|
|
|
5
5
|
interface BrowserAddressBarProps {
|
|
6
6
|
url?: string;
|
|
7
7
|
status: BrowserViewStatus;
|
|
8
|
-
viewportMode: ViewportMode;
|
|
9
8
|
zoomLevel?: number;
|
|
10
9
|
onIncludeInChat: () => void;
|
|
11
10
|
onReconnect: () => void;
|
|
12
11
|
onOpenExternal: () => void;
|
|
13
|
-
onChangeViewport: (mode: ViewportMode) => void;
|
|
14
12
|
onZoomIn: () => void;
|
|
15
13
|
onZoomOut: () => void;
|
|
16
14
|
onResetZoom: () => void;
|
|
17
|
-
onSetZoom: (zoom: number) => void;
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
const STATUS_LABEL: Record<BrowserViewStatus, string> = {
|
|
@@ -32,16 +29,13 @@ const STATUS_LABEL: Record<BrowserViewStatus, string> = {
|
|
|
32
29
|
export function BrowserAddressBar({
|
|
33
30
|
url,
|
|
34
31
|
status,
|
|
35
|
-
viewportMode,
|
|
36
32
|
zoomLevel = 100,
|
|
37
33
|
onIncludeInChat,
|
|
38
34
|
onReconnect,
|
|
39
35
|
onOpenExternal,
|
|
40
|
-
onChangeViewport,
|
|
41
36
|
onZoomIn,
|
|
42
37
|
onZoomOut,
|
|
43
38
|
onResetZoom,
|
|
44
|
-
onSetZoom,
|
|
45
39
|
}: BrowserAddressBarProps) {
|
|
46
40
|
const isLive = status === 'live';
|
|
47
41
|
|
|
@@ -50,13 +44,10 @@ export function BrowserAddressBar({
|
|
|
50
44
|
url={url ?? ''}
|
|
51
45
|
statusDotClass={isLive ? 'bg-success animate-pulse' : 'bg-ink/25'}
|
|
52
46
|
statusLabel={STATUS_LABEL[status]}
|
|
53
|
-
viewportMode={viewportMode}
|
|
54
47
|
zoomLevel={zoomLevel}
|
|
55
|
-
onChangeViewport={onChangeViewport}
|
|
56
48
|
onZoomIn={onZoomIn}
|
|
57
49
|
onZoomOut={onZoomOut}
|
|
58
50
|
onResetZoom={onResetZoom}
|
|
59
|
-
onSetZoom={onSetZoom}
|
|
60
51
|
onIncludeInChat={onIncludeInChat}
|
|
61
52
|
onOpenExternal={onOpenExternal}
|
|
62
53
|
openExternalLabel="Buka di jendela baru"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Globe, Loader2, Radio } from 'lucide-preact';
|
|
2
2
|
import { ActivityToasts } from '@/client/components/workspace/browser-panel/ActivityToasts';
|
|
3
|
-
import {
|
|
4
|
-
import type { BrowserPanelAction, BrowserTabInfo, BrowserViewStatus, ViewportMode } from '@/shared/types';
|
|
3
|
+
import type { BrowserPanelAction, BrowserTabInfo, BrowserViewStatus } from '@/shared/types';
|
|
5
4
|
|
|
6
5
|
interface BrowserScreencastProps {
|
|
7
6
|
status: BrowserViewStatus;
|
|
@@ -11,7 +10,6 @@ interface BrowserScreencastProps {
|
|
|
11
10
|
frameSrc?: string;
|
|
12
11
|
actions: BrowserPanelAction[];
|
|
13
12
|
onSelectTarget: (targetId: string) => void;
|
|
14
|
-
viewportMode: ViewportMode;
|
|
15
13
|
zoomLevel: number;
|
|
16
14
|
}
|
|
17
15
|
|
|
@@ -38,7 +36,6 @@ export function BrowserScreencast({
|
|
|
38
36
|
frameSrc,
|
|
39
37
|
actions,
|
|
40
38
|
onSelectTarget,
|
|
41
|
-
viewportMode,
|
|
42
39
|
zoomLevel,
|
|
43
40
|
}: BrowserScreencastProps) {
|
|
44
41
|
const isLive = status === 'live';
|
|
@@ -73,8 +70,10 @@ export function BrowserScreencast({
|
|
|
73
70
|
|
|
74
71
|
<div className="flex-1 flex items-center justify-center p-2 min-h-0 overflow-auto">
|
|
75
72
|
{isLive ? (
|
|
73
|
+
// Always fills the panel: the surface is a fixed-resolution JPEG, so
|
|
74
|
+
// there is no device box to frame it in — only the zoom scale.
|
|
76
75
|
<div
|
|
77
|
-
className=
|
|
76
|
+
className="relative flex flex-col bg-paper w-full h-full transition-all duration-200"
|
|
78
77
|
style={{
|
|
79
78
|
zoom: zoomLevel !== 100 ? `${zoomLevel}%` : undefined,
|
|
80
79
|
transformOrigin: 'top center',
|
|
@@ -4,7 +4,6 @@ import { BrowserScreencast } from '@/client/components/workspace/browser-panel/S
|
|
|
4
4
|
import { useScreencast } from '@/client/hooks/browser/use-screencast';
|
|
5
5
|
import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
6
6
|
import { emitBrowserPageContext } from '@/shared/lib/browser/page-context';
|
|
7
|
-
import type { ViewportMode } from '@/shared/types';
|
|
8
7
|
|
|
9
8
|
interface BrowserPanelProps {
|
|
10
9
|
className?: string;
|
|
@@ -18,9 +17,14 @@ interface BrowserPanelProps {
|
|
|
18
17
|
*
|
|
19
18
|
* This panel only mirrors what the agent does. Interactive browsing belongs to
|
|
20
19
|
* the separate user-browser panel, which runs its own private Chromium.
|
|
20
|
+
*
|
|
21
|
+
* There is deliberately NO device/viewport selector here: the surface is a
|
|
22
|
+
* fixed-resolution screencast JPEG, so a preset could only letterbox the image
|
|
23
|
+
* while claiming to emulate a device. Emulation belongs to the agent itself
|
|
24
|
+
* (`tab.emulate()` / `browser.open({ viewport })`); the user-browser panel,
|
|
25
|
+
* whose iframe's layout viewport *is* its CSS box, keeps the selector.
|
|
21
26
|
*/
|
|
22
27
|
export function BrowserPanel({ className = '', active = true }: BrowserPanelProps) {
|
|
23
|
-
const [viewportMode, setViewportMode] = useSessionState<ViewportMode>('browser.viewportMode', 'responsive');
|
|
24
28
|
const [zoomLevel, setZoomLevel] = useSessionState<number>('browser.zoomLevel', 100);
|
|
25
29
|
const { status, url, title, tabs, targetId, frameSrc, actions, selectTarget, reconnect } = useScreencast(active);
|
|
26
30
|
|
|
@@ -41,16 +45,13 @@ export function BrowserPanel({ className = '', active = true }: BrowserPanelProp
|
|
|
41
45
|
<BrowserAddressBar
|
|
42
46
|
url={url}
|
|
43
47
|
status={status}
|
|
44
|
-
viewportMode={viewportMode}
|
|
45
48
|
zoomLevel={zoomLevel}
|
|
46
49
|
onIncludeInChat={handleIncludeInChat}
|
|
47
50
|
onReconnect={reconnect}
|
|
48
51
|
onOpenExternal={handleOpenExternal}
|
|
49
|
-
onChangeViewport={setViewportMode}
|
|
50
52
|
onZoomIn={() => setZoomLevel((prev) => Math.min(200, prev + 10))}
|
|
51
53
|
onZoomOut={() => setZoomLevel((prev) => Math.max(50, prev - 10))}
|
|
52
54
|
onResetZoom={() => setZoomLevel(100)}
|
|
53
|
-
onSetZoom={setZoomLevel}
|
|
54
55
|
/>
|
|
55
56
|
|
|
56
57
|
<BrowserScreencast
|
|
@@ -61,7 +62,6 @@ export function BrowserPanel({ className = '', active = true }: BrowserPanelProp
|
|
|
61
62
|
frameSrc={frameSrc}
|
|
62
63
|
actions={actions}
|
|
63
64
|
onSelectTarget={selectTarget}
|
|
64
|
-
viewportMode={viewportMode}
|
|
65
65
|
zoomLevel={zoomLevel}
|
|
66
66
|
/>
|
|
67
67
|
</div>
|
|
@@ -13,15 +13,22 @@ export interface BrowserToolbarProps {
|
|
|
13
13
|
/** Status dot class plus its label — the only per-panel difference. */
|
|
14
14
|
statusDotClass: string;
|
|
15
15
|
statusLabel: string;
|
|
16
|
-
viewportMode: ViewportMode;
|
|
17
16
|
zoomLevel: number;
|
|
18
|
-
onChangeViewport: (mode: ViewportMode) => void;
|
|
19
17
|
onZoomIn: () => void;
|
|
20
18
|
onZoomOut: () => void;
|
|
21
19
|
onResetZoom: () => void;
|
|
22
|
-
onSetZoom: (zoom: number) => void;
|
|
23
20
|
onIncludeInChat: () => void;
|
|
24
21
|
onOpenExternal: () => void;
|
|
22
|
+
/**
|
|
23
|
+
* Device-testing cluster, rendered only when both are supplied. The USER
|
|
24
|
+
* panel passes them: its iframe's layout viewport *is* its CSS box, so a
|
|
25
|
+
* preset genuinely re-runs the page's media queries. The AGENT panel omits
|
|
26
|
+
* them — its surface is a fixed-resolution screencast JPEG, so a preset
|
|
27
|
+
* could only letterbox the image while claiming to emulate a device.
|
|
28
|
+
*/
|
|
29
|
+
viewportMode?: ViewportMode;
|
|
30
|
+
onChangeViewport?: (mode: ViewportMode) => void;
|
|
31
|
+
onSetZoom?: (zoom: number) => void;
|
|
25
32
|
/** Shown after the include/copy buttons — the agent panel's reconnect button. */
|
|
26
33
|
trailingControls?: ComponentChildren;
|
|
27
34
|
/** Open-external button label/title, differing between the two panels. */
|
|
@@ -34,9 +41,10 @@ export interface BrowserToolbarProps {
|
|
|
34
41
|
/**
|
|
35
42
|
* Shared address bar for both browser panels. The two panels differ only in
|
|
36
43
|
* their status semantics, the middle slot (read-only URL vs editable form),
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
44
|
+
* the agent panel's reconnect button, and whether the device-testing cluster
|
|
45
|
+
* is offered at all (user panel yes, agent panel no — see `viewportMode`);
|
|
46
|
+
* everything else — the include, copy, open-external buttons and the zoom
|
|
47
|
+
* cluster — lives here so the two toolbars cannot drift.
|
|
40
48
|
*/
|
|
41
49
|
export function BrowserToolbar({
|
|
42
50
|
url,
|
|
@@ -86,12 +94,14 @@ export function BrowserToolbar({
|
|
|
86
94
|
{children}
|
|
87
95
|
|
|
88
96
|
<div className="flex items-center space-x-1.5 flex-shrink-0">
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
{viewportMode && onChangeViewport && (
|
|
98
|
+
<BrowserViewportSelector
|
|
99
|
+
viewportMode={viewportMode}
|
|
100
|
+
onChangeViewport={onChangeViewport}
|
|
101
|
+
zoomLevel={zoomLevel}
|
|
102
|
+
onSetZoom={onSetZoom}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
95
105
|
|
|
96
106
|
<ZoomControls
|
|
97
107
|
zoomLevel={zoomLevel}
|
|
@@ -12,7 +12,7 @@ import { toTitleCase } from '@/shared/lib/chat/title-case';
|
|
|
12
12
|
import { FallbackOutput } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/FallbackOutput';
|
|
13
13
|
import { tryParseJson } from '@/shared/lib/code/syntax-highlight';
|
|
14
14
|
import { JsonCodeBlock } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock';
|
|
15
|
-
import { getTodoSummary } from '@/shared/lib/chat/todo
|
|
15
|
+
import { getTodoSummary } from '@/shared/lib/chat/todo/parser';
|
|
16
16
|
import { parseAskQuestions } from '@/shared/lib/chat/ask-questions';
|
|
17
17
|
|
|
18
18
|
interface ToolCallCardProps {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMemo } from 'preact/hooks';
|
|
2
2
|
import { CheckCircle2, ListTodo, Loader2, Square } from 'lucide-preact';
|
|
3
3
|
import type { ToolCallData } from '@/shared/types';
|
|
4
|
-
import { parseTodoData } from '@/shared/lib/chat/todo
|
|
4
|
+
import { parseTodoData } from '@/shared/lib/chat/todo/parser';
|
|
5
5
|
import { FallbackOutput } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/FallbackOutput';
|
|
6
6
|
|
|
7
7
|
/** Panel khusus untuk tool `todo` — task list dengan progress bar, phase groups, dan status yang readable. */
|