ompchamber 0.7.0 → 1.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 (165) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/static/css/index.e880517fcd.css +2 -0
  3. package/dist/client/static/js/async/{3y.4c2596f64c.js → 3y.71f2ccf3b6.js} +2 -2
  4. package/dist/client/static/js/async/{xw.ae67ba06fd.js → 5b.0d6ae50fe8.js} +10 -10
  5. package/dist/client/static/js/async/5bh.8b2363424f.js +1 -0
  6. package/dist/client/static/js/async/{vy.bbe114c4e0.js → bi.09688ba75c.js} +1 -2
  7. package/dist/client/static/js/async/c.797d933882.js +1 -0
  8. package/dist/client/static/js/async/{dc.6dc4b0a04b.js → dc.856fda6560.js} +2 -2
  9. package/dist/client/static/js/async/g.a06ca3b3e9.js +2 -0
  10. package/dist/client/static/js/async/g.a06ca3b3e9.js.LICENSE.txt +11 -0
  11. package/dist/client/static/js/async/{m.4b29d34f93.js → m.c038c71b9f.js} +2 -2
  12. package/dist/client/static/js/async/ma.2c261b04dd.js +2 -0
  13. package/dist/client/static/js/async/ms.98960af4eb.js +67 -0
  14. package/dist/client/static/js/async/nam.b5663e919f.js +10 -0
  15. package/dist/client/static/js/async/nam.b5663e919f.js.LICENSE.txt +11 -0
  16. package/dist/client/static/js/async/ot.84c71b308f.js +2 -0
  17. package/dist/client/static/js/async/ot.84c71b308f.js.LICENSE.txt +11 -0
  18. package/dist/client/static/js/async/{q.542c184fec.js → q.23af2a358f.js} +3 -3
  19. package/dist/client/static/js/async/{s.413c2a4683.js → s.222ee974f6.js} +2 -2
  20. package/dist/client/static/js/async/t.4dca0e2ba8.js +1 -0
  21. package/dist/client/static/js/async/uf.46ad6209fa.js +2 -0
  22. package/dist/client/static/js/async/xn.6360379f71.js +2 -0
  23. package/dist/client/static/js/async/xnl.feb0021e50.js +2 -0
  24. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +12 -0
  25. package/dist/client/static/js/index.0de15c5e17.js +33 -0
  26. package/dist/client/static/js/{uy.2e8929ed0c.js → jp.50817896f3.js} +6 -6
  27. package/package.json +2 -1
  28. package/src/client/components/common/MarkdownRenderer.tsx +69 -27
  29. package/src/client/components/common/code-editor/history.test.ts +118 -0
  30. package/src/client/components/common/code-editor/history.ts +101 -0
  31. package/src/client/components/common/code-editor/index.tsx +56 -66
  32. package/src/client/components/common/code-surface/index.tsx +84 -11
  33. package/src/client/components/common/code-surface/measure.ts +66 -17
  34. package/src/client/components/common/diagram-viewer/index.tsx +258 -0
  35. package/src/client/components/common/diagram-viewer/read-block.ts +53 -0
  36. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +2 -1
  37. package/src/client/components/layout/session-sidebar/SessionItem.tsx +11 -4
  38. package/src/client/components/layout/session-sidebar/SessionList.tsx +1 -1
  39. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +1 -1
  40. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +1 -1
  41. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +2 -2
  42. package/src/client/components/settings/LazyModal.tsx +1 -1
  43. package/src/client/components/settings/Modal.tsx +2 -1
  44. package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +11 -4
  45. package/src/client/components/workspace/chat-timeline/QueueList.tsx +18 -14
  46. package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +8 -1
  47. package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +11 -52
  48. package/src/client/components/workspace/chat-timeline/chat-input/selection.ts +57 -0
  49. package/src/client/components/workspace/chat-timeline/index.tsx +124 -108
  50. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/FrameControl.tsx +172 -0
  51. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx +155 -0
  52. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/drafts.ts +113 -0
  53. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.tsx +92 -0
  54. package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Footer.tsx +3 -3
  55. package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Header.tsx +3 -3
  56. package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Lazy.tsx +8 -8
  57. package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/SelectBody.tsx +3 -3
  58. package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/index.tsx +16 -10
  59. package/src/client/components/workspace/chat-timeline/tool-renderers/index.tsx +2 -2
  60. package/src/client/components/workspace/file-explorer/index.tsx +18 -5
  61. package/src/client/components/workspace/git-panel/BranchToolbar.tsx +2 -2
  62. package/src/client/components/workspace/git-panel/index.tsx +5 -1
  63. package/src/client/components/workspace/terminal-panel/Header.tsx +51 -19
  64. package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +86 -282
  65. package/src/client/components/workspace/terminal-panel/index.tsx +76 -50
  66. package/src/client/components/workspace/terminal-panel/mount.ts +275 -0
  67. package/src/client/data/theme/terminal.ts +27 -29
  68. package/src/client/hooks/chat/omp/session-list.tsx +23 -2
  69. package/src/client/hooks/chat/omp/session-sidebar-controller.ts +1 -1
  70. package/src/client/hooks/chat/timeline/actions.ts +23 -34
  71. package/src/client/hooks/chat/timeline/ask-frames.ts +102 -0
  72. package/src/client/hooks/chat/timeline/extension-dialog.ts +12 -9
  73. package/src/client/hooks/chat/timeline/index.ts +18 -28
  74. package/src/client/hooks/chat/timeline/prompt-text.ts +35 -0
  75. package/src/client/hooks/chat/timeline/queue.ts +110 -46
  76. package/src/client/hooks/chat/timeline/send.ts +13 -27
  77. package/src/client/hooks/chat/timeline/session-load.ts +48 -106
  78. package/src/client/hooks/chat/timeline/session-pagination.ts +144 -0
  79. package/src/client/hooks/editor/use-code-window.ts +118 -0
  80. package/src/client/hooks/ui/input-required-alert.ts +43 -0
  81. package/src/client/hooks/ui/notification-sound.ts +65 -0
  82. package/src/client/hooks/ui/theme.ts +22 -6
  83. package/src/client/hooks/workspace/terminal/index.ts +217 -0
  84. package/src/client/hooks/workspace/terminal/socket.ts +227 -0
  85. package/src/client/tailwind.css +38 -12
  86. package/src/server/db.server.ts +19 -0
  87. package/src/server/lib/fs/git-sync.ts +191 -0
  88. package/src/server/lib/omp/rpc/manager.ts +72 -6
  89. package/src/server/lib/omp/rpc/session-commands.ts +6 -0
  90. package/src/server/lib/omp/rpc/session-registry.ts +18 -0
  91. package/src/server/lib/omp/session/sidebar-data.server.ts +10 -4
  92. package/src/server/lib/queue/delivery.server.ts +114 -0
  93. package/src/server/lib/queue/schema-migration.server.ts +50 -0
  94. package/src/server/lib/queue/store.server.ts +185 -0
  95. package/src/server/lib/terminal/foreground.ts +73 -0
  96. package/src/server/lib/terminal/history.test.ts +118 -0
  97. package/src/server/lib/terminal/history.ts +205 -0
  98. package/src/server/lib/terminal/runtime.server.ts +338 -0
  99. package/src/server/lib/terminal/session.server.ts +215 -0
  100. package/src/server/lib/terminal/shell.test.ts +97 -0
  101. package/src/server/lib/terminal/shell.ts +103 -0
  102. package/src/server/routes/fs/git.ts +22 -12
  103. package/src/server/routes/index.ts +2 -1
  104. package/src/server/routes/sessions/index.ts +11 -3
  105. package/src/server/routes/sessions/queue.ts +121 -0
  106. package/src/server/routes/sessions/session.ts +1 -132
  107. package/src/server/routes/settings/commands.ts +1 -1
  108. package/src/server/routes/terminal/index.ts +2 -2
  109. package/src/server/routes/terminal/run.ts +8 -100
  110. package/src/server/routes/terminal/sessions.ts +24 -0
  111. package/src/server/routes/terminal/ws.ts +149 -0
  112. package/src/shared/lib/chat/ask-questions.test.ts +169 -0
  113. package/src/shared/lib/chat/ask-questions.ts +264 -0
  114. package/src/shared/lib/chat/omp/agent-events.test.ts +37 -1
  115. package/src/shared/lib/chat/omp/agent-events.ts +30 -0
  116. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +9 -1
  117. package/src/shared/lib/chat/timeline/omp-callbacks.ts +45 -8
  118. package/src/shared/lib/code/lazy-window.test.ts +161 -0
  119. package/src/shared/lib/code/lazy-window.ts +156 -0
  120. package/src/shared/lib/code/syntax-highlight.test.ts +50 -1
  121. package/src/shared/lib/code/syntax-highlight.ts +14 -1
  122. package/src/shared/lib/code/windowed-highlight.ts +63 -0
  123. package/src/shared/lib/markdown/mermaid.ts +8 -6
  124. package/src/shared/lib/omp/session/stream-state.server.ts +2 -3
  125. package/src/shared/lib/workspace/session-state/store.ts +1 -4
  126. package/src/shared/lib/workspace/terminal/protocol.ts +222 -0
  127. package/src/shared/types/omp/agent.ts +14 -2
  128. package/src/shared/types/terminal.ts +17 -15
  129. package/src/shared/types/workspace.ts +6 -2
  130. package/dist/client/static/css/index.51f68210a5.css +0 -2
  131. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  132. package/dist/client/static/js/async/c.cd45b0ac45.js +0 -1
  133. package/dist/client/static/js/async/fw.6a51998a8e.js +0 -6
  134. package/dist/client/static/js/async/g.86e2256a70.js +0 -2
  135. package/dist/client/static/js/async/kc.bbdb35b771.js +0 -1
  136. package/dist/client/static/js/async/mw.ca1512563c.js +0 -10
  137. package/dist/client/static/js/async/rf.d626afc367.js +0 -2
  138. package/dist/client/static/js/async/t.cda4891526.js +0 -1
  139. package/dist/client/static/js/async/uf.24ca3d49c3.js +0 -2
  140. package/dist/client/static/js/async/uf.24ca3d49c3.js.LICENSE.txt +0 -6
  141. package/dist/client/static/js/async/v0.394ed37c6b.js +0 -2
  142. package/dist/client/static/js/async/v0.394ed37c6b.js.LICENSE.txt +0 -6
  143. package/dist/client/static/js/async/vy.bbe114c4e0.js.LICENSE.txt +0 -6
  144. package/dist/client/static/js/async/xn.afecebbbd9.js +0 -2
  145. package/dist/client/static/js/index.eccad0b70a.js +0 -33
  146. package/dist/client/static/js/index.eccad0b70a.js.LICENSE.txt +0 -4
  147. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Ask.tsx +0 -205
  148. package/src/client/components/workspace/terminal-panel/InputBar.tsx +0 -82
  149. package/src/client/components/workspace/terminal-panel/LogEntry.tsx +0 -70
  150. package/src/client/hooks/workspace/terminal-output.ts +0 -66
  151. package/src/client/hooks/workspace/terminal.ts +0 -283
  152. package/src/server/lib/fs/terminal-cwd.ts +0 -49
  153. package/src/server/routes/terminal/stream.ts +0 -100
  154. /package/dist/client/static/js/async/{3y.4c2596f64c.js.LICENSE.txt → 3y.71f2ccf3b6.js.LICENSE.txt} +0 -0
  155. /package/dist/client/static/js/async/{g.86e2256a70.js.LICENSE.txt → 5b.0d6ae50fe8.js.LICENSE.txt} +0 -0
  156. /package/dist/client/static/js/async/{dc.6dc4b0a04b.js.LICENSE.txt → dc.856fda6560.js.LICENSE.txt} +0 -0
  157. /package/dist/client/static/js/async/{fw.6a51998a8e.js.LICENSE.txt → m.c038c71b9f.js.LICENSE.txt} +0 -0
  158. /package/dist/client/static/js/async/{m.4b29d34f93.js.LICENSE.txt → ma.2c261b04dd.js.LICENSE.txt} +0 -0
  159. /package/dist/client/static/js/async/{xn.afecebbbd9.js.LICENSE.txt → ms.98960af4eb.js.LICENSE.txt} +0 -0
  160. /package/dist/client/static/js/async/{mw.ca1512563c.js.LICENSE.txt → q.23af2a358f.js.LICENSE.txt} +0 -0
  161. /package/dist/client/static/js/async/{q.542c184fec.js.LICENSE.txt → s.222ee974f6.js.LICENSE.txt} +0 -0
  162. /package/dist/client/static/js/async/{rf.d626afc367.js.LICENSE.txt → uf.46ad6209fa.js.LICENSE.txt} +0 -0
  163. /package/dist/client/static/js/async/{s.413c2a4683.js.LICENSE.txt → xn.6360379f71.js.LICENSE.txt} +0 -0
  164. /package/dist/client/static/js/{async/xw.ae67ba06fd.js.LICENSE.txt → index.0de15c5e17.js.LICENSE.txt} +0 -0
  165. /package/dist/client/static/js/{uy.2e8929ed0c.js.LICENSE.txt → jp.50817896f3.js.LICENSE.txt} +0 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The control for the dialog omp is currently blocked on.
8
+ *
9
+ * The option list is rendered from the FRAME, never from the tool arguments:
10
+ * omp decorates it on its way out — ` (Recommended)` on the suggested choice,
11
+ * `Other (type your own)` appended to every question, and whichever label ends a
12
+ * multi-select sweep — and those exact strings are what omp compares a response
13
+ * against. Reproducing them locally would only risk sending a label omp does not
14
+ * recognise.
15
+ */
16
+
17
+ import { useEffect, useState } from 'preact/hooks';
18
+ import { Check, CornerDownLeft } from 'lucide-preact';
19
+ import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
20
+ import { isOtherOption } from '@/shared/lib/chat/ask-questions';
21
+ import type { ExtensionDialogResponse } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/Lazy';
22
+
23
+ interface FrameControlProps {
24
+ frame: ExtensionUiDialogRequest;
25
+ /** Question picks several options, so every row toggles instead of answering. */
26
+ multi: boolean;
27
+ /** Labels already toggled on a multi-select question — display only. */
28
+ selected: string[];
29
+ /** Commit a single-choice answer; sent as soon as omp is listening for it. */
30
+ onAnswer: (value: string) => void;
31
+ /** Send one multi-select toggle. */
32
+ onToggle: (label: string) => void;
33
+ /** Answer a dialog that is not a choice list (confirm / input / editor). */
34
+ onRespond: (response: ExtensionDialogResponse) => void;
35
+ }
36
+
37
+ const RECOMMENDED = / \(recommended\)$/i;
38
+ const DONE_ROW = /\bdone\b/i;
39
+
40
+ export function FrameControl({ frame, multi, selected, onAnswer, onToggle, onRespond }: FrameControlProps) {
41
+ const [text, setText] = useState('');
42
+ // A new dialog reuses this component; text left over from the previous one
43
+ // must not read as an answer the user just typed.
44
+ useEffect(() => {
45
+ setText(frame.method === 'editor' ? frame.prefill ?? '' : '');
46
+ }, [frame.id, frame.method, frame.prefill]);
47
+
48
+ if (frame.method === 'confirm') {
49
+ return (
50
+ <div className="mt-2 flex items-center gap-2">
51
+ <button
52
+ type="button"
53
+ onClick={() => onRespond({ confirmed: true })}
54
+ className="flex cursor-pointer items-center gap-1.5 rounded-md bg-ink px-3 py-1 text-[11.5px] font-semibold text-paper transition-opacity hover:opacity-90"
55
+ >
56
+ <Check size={11} />
57
+ <span>Confirm</span>
58
+ </button>
59
+ <button
60
+ type="button"
61
+ onClick={() => onRespond({ confirmed: false })}
62
+ className="cursor-pointer rounded-md border border-ink/15 px-3 py-1 text-[11.5px] font-medium text-ink/70 transition-colors hover:border-ink/30 hover:text-ink"
63
+ >
64
+ Decline
65
+ </button>
66
+ </div>
67
+ );
68
+ }
69
+
70
+ if (frame.method === 'input' || frame.method === 'editor') {
71
+ return (
72
+ <div className="mt-2 space-y-1.5">
73
+ {frame.title && <div className="text-[10.5px] text-ink/45">{frame.title}</div>}
74
+ {frame.method === 'editor' ? (
75
+ <textarea
76
+ autoFocus
77
+ value={text}
78
+ onChange={(event) => setText(event.currentTarget.value)}
79
+ rows={4}
80
+ placeholder={frame.placeholder || 'Type your answer…'}
81
+ className="w-full rounded-md border border-ink/15 bg-canvas/40 px-2.5 py-1.5 font-mono text-[11.5px] leading-relaxed text-ink outline-none focus:border-ink"
82
+ />
83
+ ) : (
84
+ <input
85
+ autoFocus
86
+ value={text}
87
+ placeholder={frame.placeholder || 'Type your answer…'}
88
+ onChange={(event) => setText(event.currentTarget.value)}
89
+ onKeyDown={(event) => {
90
+ if (event.key !== 'Enter') return;
91
+ event.preventDefault();
92
+ onRespond({ value: text });
93
+ }}
94
+ className="w-full rounded-md border border-ink/15 bg-canvas/40 px-2.5 py-1.5 text-[11.5px] text-ink outline-none focus:border-ink"
95
+ />
96
+ )}
97
+ <button
98
+ type="button"
99
+ onClick={() => onRespond({ value: text })}
100
+ className="flex cursor-pointer items-center gap-1.5 rounded-md bg-ink px-3 py-1 text-[11.5px] font-semibold text-paper transition-opacity hover:opacity-90"
101
+ >
102
+ <span>Submit</span>
103
+ <CornerDownLeft size={11} className="opacity-70" />
104
+ </button>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ const options = frame.options ?? [];
110
+ const details = frame.optionDetails ?? [];
111
+ if (frame.method !== 'select' || options.length === 0) return null;
112
+
113
+ return (
114
+ <div className="mt-2 space-y-1" role={multi ? 'group' : 'radiogroup'}>
115
+ {options.map((label, index) => {
116
+ // A single-choice question resolves "Other" to the typed text, so the
117
+ // row itself is a prompt to type — never a literal answer.
118
+ if (!multi && isOtherOption(label)) return null;
119
+ const description = details[index]?.description;
120
+ const checked = multi && selected.includes(label);
121
+ const finish = multi && DONE_ROW.test(label);
122
+ return (
123
+ <button
124
+ key={label}
125
+ type="button"
126
+ onClick={() => (multi ? (finish ? onRespond({ value: label }) : onToggle(label)) : onAnswer(label))}
127
+ className={`flex w-full cursor-pointer items-start gap-2 rounded-md border px-2.5 py-1.5 text-left text-[11.5px] transition-colors ${
128
+ checked
129
+ ? 'border-ink/30 bg-ink/5 text-ink'
130
+ : finish
131
+ ? 'border-ink/20 bg-canvas/60 font-medium text-ink hover:border-ink/40'
132
+ : 'border-ink/8 bg-canvas/30 text-ink/80 hover:border-ink/25 hover:bg-ink/5 hover:text-ink'
133
+ }`}
134
+ >
135
+ <span
136
+ className={`mt-1 h-3 w-3 shrink-0 ${multi ? 'rounded-[3px]' : 'rounded-full'} border ${
137
+ checked ? 'border-ink bg-ink' : 'border-ink/30'
138
+ }`}
139
+ />
140
+ <span className="min-w-0 flex-1">
141
+ <span className="flex items-center gap-1.5 font-medium">
142
+ <span className="truncate">{label}</span>
143
+ {RECOMMENDED.test(label) && !multi && (
144
+ <span className="shrink-0 rounded-full bg-ink/5 px-1.5 py-px font-mono text-[9px] uppercase tracking-wider text-ink/45">
145
+ Suggested
146
+ </span>
147
+ )}
148
+ </span>
149
+ {description && (
150
+ <span className="mt-0.5 block text-[10.5px] leading-relaxed text-ink/50">{description}</span>
151
+ )}
152
+ </span>
153
+ </button>
154
+ );
155
+ })}
156
+
157
+ {!multi && (
158
+ <input
159
+ value={text}
160
+ placeholder="Other — type your own answer…"
161
+ onChange={(event) => setText(event.currentTarget.value)}
162
+ onKeyDown={(event) => {
163
+ if (event.key !== 'Enter' || !text.trim()) return;
164
+ event.preventDefault();
165
+ onAnswer(text.trim());
166
+ }}
167
+ className="w-full rounded-md border border-ink/15 bg-canvas/40 px-2.5 py-1.5 text-[11.5px] text-ink outline-none focus:border-ink"
168
+ />
169
+ )}
170
+ </div>
171
+ );
172
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * One question of an `ask` call, in whichever of its three states it is in:
8
+ * answered (omp already recorded it), asking (its dialog is live) or still
9
+ * queued behind an earlier question — omp asks them strictly in order, so the
10
+ * later ones can only be previewed until their turn comes.
11
+ */
12
+
13
+ import { CheckCircle2, Clock } from 'lucide-preact';
14
+ import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
15
+ import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
16
+ import type { AskAnswer, AskQuestion } from '@/shared/lib/chat/ask-questions';
17
+ import { FrameControl } from '@/client/components/workspace/chat-timeline/tool-renderers/ask-panel/FrameControl';
18
+ import type { ExtensionDialogResponse } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/Lazy';
19
+
20
+ interface QuestionBlockProps {
21
+ index: number;
22
+ total: number;
23
+ question: AskQuestion;
24
+ /** Dialogs this question raised, oldest first; the last one is live. */
25
+ frames: ExtensionUiDialogRequest[];
26
+ recorded: AskAnswer | null;
27
+ /** Answer picked locally for a question omp has not reached yet. */
28
+ draft?: string;
29
+ /** Unanswered, but omp has already moved past it — its dialog is gone. */
30
+ passed: boolean;
31
+ /** Labels toggled locally on a multi-select question. */
32
+ multiSelected: string[];
33
+ onAnswer: (value: string) => void;
34
+ onToggle: (label: string) => void;
35
+ onRespond: (response: ExtensionDialogResponse) => void;
36
+ }
37
+
38
+ function answerParts(answer: AskAnswer): string[] {
39
+ const parts = [...answer.selectedOptions];
40
+ if (answer.customInput) parts.push(answer.customInput);
41
+ return parts;
42
+ }
43
+
44
+ export function QuestionBlock({
45
+ index,
46
+ total,
47
+ question,
48
+ frames,
49
+ recorded,
50
+ draft,
51
+ passed,
52
+ multiSelected,
53
+ onAnswer,
54
+ onToggle,
55
+ onRespond,
56
+ }: QuestionBlockProps) {
57
+ const live = frames.length > 0 ? frames[frames.length - 1] : null;
58
+ const answered = recorded ? answerParts(recorded) : draft ? [draft] : multiSelected;
59
+ const asking = Boolean(live) && !recorded && (question.multi || !draft);
60
+
61
+ return (
62
+ <div className="rounded-lg border border-ink/10 bg-paper p-2.5 shadow-xs">
63
+ <div className="flex items-start gap-2">
64
+ <span className="mt-px flex h-4 w-4 shrink-0 items-center justify-center rounded bg-ink/5 font-mono text-[9px] font-semibold text-ink/60">
65
+ {index + 1}
66
+ </span>
67
+ <div className="min-w-0 flex-1">
68
+ <div className="flex items-center gap-1.5">
69
+ <span className="text-[9px] font-semibold uppercase tracking-wider text-ink/40">
70
+ {question.header ?? `Question ${index + 1}`}
71
+ </span>
72
+ {question.multi && (
73
+ <span className="rounded-full bg-ink/5 px-1.5 py-px font-mono text-[8.5px] text-ink/45">multi</span>
74
+ )}
75
+ {total > 1 && (
76
+ <span className="ml-auto font-mono text-[9px] text-ink/35">
77
+ {index + 1}/{total}
78
+ </span>
79
+ )}
80
+ </div>
81
+
82
+ <div className="mt-1 text-[12.5px] leading-relaxed text-ink/90 select-text">
83
+ <MarkdownRenderer content={question.question} />
84
+ </div>
85
+
86
+ {asking && live && (
87
+ <FrameControl
88
+ frame={live}
89
+ multi={question.multi}
90
+ selected={multiSelected}
91
+ onAnswer={onAnswer}
92
+ onToggle={onToggle}
93
+ onRespond={onRespond}
94
+ />
95
+ )}
96
+
97
+ {answered.length > 0 && (
98
+ <div className="mt-2 flex flex-wrap items-center gap-1.5">
99
+ <CheckCircle2 size={11} className="shrink-0 text-success" />
100
+ {answered.map((part) => (
101
+ <span
102
+ key={part}
103
+ className="rounded-md border border-ink/12 bg-canvas/40 px-2 py-0.5 text-[11.5px] font-medium text-ink select-text"
104
+ >
105
+ {part}
106
+ </span>
107
+ ))}
108
+ {recorded?.timedOut && (
109
+ <span className="rounded-full bg-ink/5 px-1.5 py-px font-mono text-[9px] text-ink/45">
110
+ auto-selected on timeout
111
+ </span>
112
+ )}
113
+ </div>
114
+ )}
115
+
116
+ {!asking && answered.length === 0 && passed && (
117
+ <div className="mt-2 flex items-center gap-1.5 text-[10.5px] text-ink/45">
118
+ <CheckCircle2 size={11} className="shrink-0 text-ink/35" />
119
+ <span>Answered earlier in this run.</span>
120
+ </div>
121
+ )}
122
+
123
+ {!asking && answered.length === 0 && !passed && (
124
+ <div className="mt-2 space-y-1 border-t border-ink/6 pt-2">
125
+ {question.options.map((option, optionIndex) => (
126
+ <div key={option.label} className="flex items-start gap-2 px-0.5 text-[11.5px] text-ink/45">
127
+ <span className="mt-1 h-3 w-3 shrink-0 rounded-full border border-ink/20" />
128
+ <span className="min-w-0 flex-1">
129
+ <span className="block truncate">
130
+ {option.label}
131
+ {question.recommended === optionIndex && (
132
+ <span className="ml-1.5 font-mono text-[9px] uppercase tracking-wider text-ink/35">
133
+ suggested
134
+ </span>
135
+ )}
136
+ </span>
137
+ {option.description && (
138
+ <span className="mt-0.5 block text-[10.5px] leading-relaxed text-ink/35">
139
+ {option.description}
140
+ </span>
141
+ )}
142
+ </span>
143
+ </div>
144
+ ))}
145
+ <div className="flex items-center gap-1.5 pt-0.5 text-[10.5px] text-ink/40">
146
+ <Clock size={10} className="shrink-0" />
147
+ <span>Waiting for the previous answer…</span>
148
+ </div>
149
+ </div>
150
+ )}
151
+ </div>
152
+ </div>
153
+ </div>
154
+ );
155
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Answer bookkeeping for the inline ask card.
8
+ *
9
+ * omp asks a multi-question `ask` one question at a time and blocks on each, so
10
+ * only the question currently in flight owns a dialog. The card still lets the
11
+ * user answer any of them, in any order: a choice made for a question omp has
12
+ * not reached yet is held here and released the moment its dialog arrives. Every
13
+ * sent dialog is remembered by id, so a re-render (or a replayed request after a
14
+ * page reload) can never answer the same frame twice.
15
+ *
16
+ * Multi-select questions are excluded from that queue: omp re-asks them once per
17
+ * toggle and only the LAST one settles the question, so each click is sent
18
+ * immediately and only the display state accumulates here.
19
+ */
20
+
21
+ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
22
+ import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
23
+ import type { AskAnswer, AskQuestion } from '@/shared/lib/chat/ask-questions';
24
+ import type { ExtensionDialogResponse } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/Lazy';
25
+
26
+ interface UseAskDraftsOptions {
27
+ toolId: string;
28
+ questions: AskQuestion[];
29
+ /** Dialogs per question, oldest first — the last one is the live dialog. */
30
+ groups: ExtensionUiDialogRequest[][];
31
+ /** Answers omp already recorded in the tool result, per question. */
32
+ recorded: (AskAnswer | null)[];
33
+ respond: (request: ExtensionUiDialogRequest, response: ExtensionDialogResponse) => void;
34
+ }
35
+
36
+ export interface AskDrafts {
37
+ /** Committed single-choice answers, by question index. */
38
+ drafts: Record<number, string>;
39
+ /** Labels the user has toggled on a multi-select question, for display only. */
40
+ multiSelected: Record<number, string[]>;
41
+ commit: (index: number, value: string) => void;
42
+ toggleMulti: (index: number, label: string) => void;
43
+ }
44
+
45
+ /** The live dialog for a question: the most recent frame it raised. */
46
+ export function liveFrame(groups: ExtensionUiDialogRequest[][], index: number): ExtensionUiDialogRequest | null {
47
+ const frames = groups[index];
48
+ return frames && frames.length > 0 ? frames[frames.length - 1] : null;
49
+ }
50
+
51
+ // omp records answers only in the FINAL tool result, so a reload mid-ask would
52
+ // otherwise repaint every question already answered as still waiting. The draft
53
+ // cache keeps the text across a reload; it is per-tab and dies with the session.
54
+ function draftKey(toolId: string): string {
55
+ return `ompchamber.ask-draft:${toolId}`;
56
+ }
57
+
58
+ function loadDrafts(toolId: string): Record<number, string> {
59
+ try {
60
+ const raw = sessionStorage.getItem(draftKey(toolId));
61
+ return raw ? (JSON.parse(raw) as Record<number, string>) : {};
62
+ } catch {
63
+ return {};
64
+ }
65
+ }
66
+
67
+ export function useAskDrafts({ toolId, questions, groups, recorded, respond }: UseAskDraftsOptions): AskDrafts {
68
+ const [drafts, setDrafts] = useState<Record<number, string>>(() => loadDrafts(toolId));
69
+ const [multiSelected, setMultiSelected] = useState<Record<number, string[]>>({});
70
+ const sentRef = useRef<Set<string>>(new Set());
71
+
72
+ useEffect(() => {
73
+ for (let index = 0; index < questions.length; index += 1) {
74
+ if (recorded[index] || questions[index].multi) continue;
75
+ const value = drafts[index];
76
+ if (!value) continue;
77
+ const frame = liveFrame(groups, index);
78
+ if (!frame || frame.method !== 'select' || sentRef.current.has(frame.id)) continue;
79
+ sentRef.current.add(frame.id);
80
+ respond(frame, { value });
81
+ }
82
+ }, [questions, groups, drafts, recorded, respond]);
83
+
84
+ useEffect(() => {
85
+ try {
86
+ sessionStorage.setItem(draftKey(toolId), JSON.stringify(drafts));
87
+ } catch {
88
+ // Storage disabled (private mode): the card still works for this mount.
89
+ }
90
+ }, [toolId, drafts]);
91
+
92
+ const commit = useCallback((index: number, value: string) => {
93
+ setDrafts((current) => (current[index] === value ? current : { ...current, [index]: value }));
94
+ }, []);
95
+
96
+ const toggleMulti = useCallback((index: number, label: string) => {
97
+ const frame = liveFrame(groups, index);
98
+ if (!frame || sentRef.current.has(frame.id)) return;
99
+ // One omp dialog per toggle: answering it re-asks the question with the
100
+ // updated set, so the next click lands on the dialog that just arrived.
101
+ sentRef.current.add(frame.id);
102
+ respond(frame, { value: label });
103
+ setMultiSelected((current) => {
104
+ const selected = current[index] ?? [];
105
+ return {
106
+ ...current,
107
+ [index]: selected.includes(label) ? selected.filter((item) => item !== label) : [...selected, label],
108
+ };
109
+ });
110
+ }, [groups, respond]);
111
+
112
+ return { drafts, multiSelected, commit, toggleMulti };
113
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Inline card for omp's `ask` tool — the whole conversation with the user, in
8
+ * the timeline instead of a modal.
9
+ *
10
+ * omp asks one question per dialog and blocks on it, so a modal could only ever
11
+ * show the question in flight and lost every earlier one on the way; the card
12
+ * keeps all of them in view, marks the ones already answered, and ends as the
13
+ * summary of what the agent asked and what it was told.
14
+ */
15
+
16
+ import { HelpCircle, XCircle } from 'lucide-preact';
17
+ import type { ToolCallData } from '@/shared/types';
18
+ import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
19
+ import { parseAskQuestions, parseAskResult } from '@/shared/lib/chat/ask-questions';
20
+ import { useAskFrames } from '@/client/hooks/chat/timeline/ask-frames';
21
+ import { liveFrame, useAskDrafts } from '@/client/components/workspace/chat-timeline/tool-renderers/ask-panel/drafts';
22
+ import { QuestionBlock } from '@/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock';
23
+ import { FallbackOutput } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/FallbackOutput';
24
+
25
+ // Stable empties: fresh `[]` literals would defeat the question block's props.
26
+ const NO_FRAMES: ExtensionUiDialogRequest[] = [];
27
+ const NO_GROUPS: ExtensionUiDialogRequest[][] = [];
28
+ const NO_LABELS: string[] = [];
29
+
30
+ export function AskPanel({ tool }: { tool: ToolCallData }) {
31
+ const questions = parseAskQuestions(tool);
32
+ const recorded = parseAskResult(tool, questions);
33
+ const { framesByTool, respond } = useAskFrames();
34
+ const groups = framesByTool.get(tool.id) ?? NO_GROUPS;
35
+ const { drafts, multiSelected, commit, toggleMulti } = useAskDrafts({
36
+ toolId: tool.id,
37
+ questions,
38
+ groups,
39
+ recorded,
40
+ respond,
41
+ });
42
+
43
+ // An `ask` whose arguments the timeline never saw (or a non-questions shape)
44
+ // still has a result worth showing.
45
+ if (questions.length === 0) {
46
+ return tool.output ? <FallbackOutput text={tool.output} /> : null;
47
+ }
48
+
49
+ const waiting = groups.some((frames, index) => frames.length > 0 && !recorded[index]);
50
+ // omp asks in order and blocks on each, so the earliest question holding a
51
+ // dialog is the only open one: everything before it is answered, whether or
52
+ // not this client still has the text.
53
+ const askingIndex = groups.findIndex((frames) => frames.length > 0);
54
+
55
+ return (
56
+ <div className="space-y-2">
57
+ {waiting && (
58
+ <div className="flex items-center gap-1.5 text-[10.5px] text-ink/50">
59
+ <HelpCircle size={11} className="shrink-0" />
60
+ <span>Input needed — the agent is blocked until you answer.</span>
61
+ </div>
62
+ )}
63
+
64
+ {questions.map((question, index) => (
65
+ <QuestionBlock
66
+ key={question.id ?? index}
67
+ index={index}
68
+ total={questions.length}
69
+ question={question}
70
+ frames={groups[index] ?? NO_FRAMES}
71
+ recorded={recorded[index] ?? null}
72
+ draft={drafts[index]}
73
+ passed={askingIndex !== -1 && index < askingIndex}
74
+ multiSelected={multiSelected[index] ?? NO_LABELS}
75
+ onAnswer={(value) => commit(index, value)}
76
+ onToggle={(label) => toggleMulti(index, label)}
77
+ onRespond={(response) => {
78
+ const frame = liveFrame(groups, index);
79
+ if (frame) respond(frame, response);
80
+ }}
81
+ />
82
+ ))}
83
+
84
+ {(tool.status === 'error' || tool.status === 'aborted') && tool.output && (
85
+ <div className="flex items-start gap-2 rounded-md border border-ink/8 bg-ink/[0.02] px-2.5 py-1.5 text-[10.5px] text-ink/60">
86
+ <XCircle size={11} className="mt-0.5 shrink-0 text-ink/40" />
87
+ <span className="font-mono select-text">{tool.output}</span>
88
+ </div>
89
+ )}
90
+ </div>
91
+ );
92
+ }
@@ -1,6 +1,6 @@
1
1
  import { CornerDownLeft, X } from 'lucide-preact';
2
2
 
3
- interface AskDialogFooterProps {
3
+ interface ExtensionDialogFooterProps {
4
4
  method: string;
5
5
  optionsLength: number;
6
6
  selectedOption: string | null;
@@ -9,14 +9,14 @@ interface AskDialogFooterProps {
9
9
  onSubmit: () => void;
10
10
  }
11
11
 
12
- export function AskDialogFooter({
12
+ export function ExtensionDialogFooter({
13
13
  method,
14
14
  optionsLength,
15
15
  selectedOption,
16
16
  hasCustomAnswer = false,
17
17
  onCancel,
18
18
  onSubmit,
19
- }: AskDialogFooterProps) {
19
+ }: ExtensionDialogFooterProps) {
20
20
  const isSelectDisabled = method === 'select' && !selectedOption && !hasCustomAnswer;
21
21
 
22
22
  return (
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from 'preact/compat';
2
2
  import { Sparkles, X } from 'lucide-preact';
3
3
 
4
- interface AskDialogHeaderProps {
4
+ interface ExtensionDialogHeaderProps {
5
5
  title?: string;
6
6
  methodLabel: string;
7
7
  badge: string;
@@ -9,13 +9,13 @@ interface AskDialogHeaderProps {
9
9
  onCancel: () => void;
10
10
  }
11
11
 
12
- export function AskDialogHeader({
12
+ export function ExtensionDialogHeader({
13
13
  title,
14
14
  methodLabel,
15
15
  badge,
16
16
  icon,
17
17
  onCancel,
18
- }: AskDialogHeaderProps) {
18
+ }: ExtensionDialogHeaderProps) {
19
19
  return (
20
20
  <div className="flex items-center justify-between border-b border-ink/8 bg-canvas/40 px-5 py-3.5">
21
21
  <div className="flex items-center gap-3 min-w-0">
@@ -1,24 +1,24 @@
1
1
  import { Suspense, lazy } from 'preact/compat';
2
2
  import type { ExtensionUiDialogRequest } from '@/client/hooks/chat/omp';
3
- import type { ExtensionDialogResponse } from '@/client/components/workspace/chat-timeline/tool-renderers/ask-dialog/index';
3
+ import type { ExtensionDialogResponse } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/index';
4
4
 
5
- const AskDialogImpl = lazy(() =>
6
- import('@/client/components/workspace/chat-timeline/tool-renderers/ask-dialog/index').then((m) => ({
7
- default: m.AskDialog,
5
+ const ExtensionDialogImpl = lazy(() =>
6
+ import('@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/index').then((m) => ({
7
+ default: m.ExtensionDialog,
8
8
  }))
9
9
  );
10
10
 
11
- interface AskDialogProps {
11
+ interface ExtensionDialogProps {
12
12
  request: ExtensionUiDialogRequest;
13
13
  onRespond: (request: ExtensionUiDialogRequest, response: ExtensionDialogResponse) => void;
14
14
  }
15
15
 
16
- export function AskDialog(props: AskDialogProps) {
16
+ export function ExtensionDialog(props: ExtensionDialogProps) {
17
17
  return (
18
18
  <Suspense fallback={null}>
19
- <AskDialogImpl {...props} />
19
+ <ExtensionDialogImpl {...props} />
20
20
  </Suspense>
21
21
  );
22
22
  }
23
23
 
24
- export type { ExtensionDialogResponse } from '@/client/components/workspace/chat-timeline/tool-renderers/ask-dialog/index';
24
+ export type { ExtensionDialogResponse } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/index';
@@ -1,7 +1,7 @@
1
1
  import type { RefObject } from 'preact/compat';
2
2
  import { CheckCircle2, Circle } from 'lucide-preact';
3
3
 
4
- interface AskDialogSelectBodyProps {
4
+ interface ExtensionDialogSelectBodyProps {
5
5
  options: string[];
6
6
  optionDetails: { description?: string }[];
7
7
  selectedOption: string | null;
@@ -12,7 +12,7 @@ interface AskDialogSelectBodyProps {
12
12
  onConfirmOption: (option: string) => void;
13
13
  }
14
14
 
15
- export function AskDialogSelectBody({
15
+ export function ExtensionDialogSelectBody({
16
16
  options,
17
17
  optionDetails,
18
18
  selectedOption,
@@ -21,7 +21,7 @@ export function AskDialogSelectBody({
21
21
  customInputRef,
22
22
  onSelect,
23
23
  onConfirmOption,
24
- }: AskDialogSelectBodyProps) {
24
+ }: ExtensionDialogSelectBodyProps) {
25
25
  return (
26
26
  <div className="space-y-2">
27
27
  <div className="flex items-center justify-between text-[11px] font-medium tracking-wider text-ink/50 uppercase">