@cueloop/client 0.1.0-alpha.102 → 0.1.0-alpha.104
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/package.json +5 -5
- package/src/annotations/annotation-palette.ts +1 -0
- package/src/annotations/components/AnnotationCards.tsx +4 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +3 -0
- package/src/annotations/discussions.ts +1 -0
- package/src/annotations/thread-selection.ts +6 -0
- package/src/annotations/use-annotation-surface.tsx +97 -0
- package/src/app/App.deleted-scroll.test.tsx +3 -0
- package/src/app/App.harness-settings.test.tsx +77 -0
- package/src/app/App.plan-render.test.tsx +2 -0
- package/src/app/App.plan-review.test.tsx +1 -0
- package/src/app/App.readonly.test.tsx +63 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.tsx +94 -12
- package/src/app/App.visual.test.tsx +1 -0
- package/src/app/App.walk.test.tsx +1 -0
- package/src/app/App.workbench.test.tsx +1 -0
- package/src/app/app-screens.tsx +3 -2
- package/src/app/app-view-model.ts +11 -1
- package/src/app/components/WorkspacePanels.tsx +1 -0
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/app/intent-dispatch.ts +26 -0
- package/src/app/ready-signal.ts +2 -0
- package/src/app/run.ts +2 -0
- package/src/app/serve.test.ts +4 -0
- package/src/diff/components/AnnotatableFileView.tsx +1 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +9 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +25 -0
- package/src/diff/components/diff-content-view-layout.ts +3 -0
- package/src/diff/diff-fold.ts +4 -0
- package/src/diff/diff-hunk-curate.ts +3 -0
- package/src/diff/diff-intraline.ts +6 -0
- package/src/diff/diff-syntax.ts +8 -0
- package/src/diff/split-diff.ts +6 -0
- package/src/diff/view-diff.ts +18 -0
- package/src/diff/walk.ts +3 -0
- package/src/index.ts +1 -0
- package/src/integrations/attribution.ts +2 -0
- package/src/integrations/client-extension-registry.ts +4 -0
- package/src/integrations/clipboard.ts +1 -0
- package/src/integrations/components/ShareDialog.agent.test.tsx +2 -0
- package/src/integrations/components/ShareDialog.tsx +28 -0
- package/src/integrations/editor.ts +3 -0
- package/src/integrations/github-identity.test.ts +1 -0
- package/src/integrations/github-identity.ts +1 -0
- package/src/integrations/herdr.ts +2 -0
- package/src/integrations/pasted-image.ts +2 -0
- package/src/integrations/share.ts +46 -1
- package/src/keyboard/components/HandleChipsInput.tsx +4 -0
- package/src/keyboard/components/SlashComposer.tsx +3 -0
- package/src/keyboard/key-bindings.ts +3 -0
- package/src/keyboard/keymap.ts +51 -0
- package/src/keyboard/skills.ts +2 -0
- package/src/keyboard/slash-palette.ts +8 -0
- package/src/keyboard/thread-chords.ts +5 -0
- package/src/keyboard/use-shared-keyboard.ts +1 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +3 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +3 -0
- package/src/markdown/components/ThreadView.tsx +13 -0
- package/src/markdown/mark-runs.ts +4 -0
- package/src/markdown/markdown-editor-syntax-style.ts +1 -0
- package/src/markdown/markdown-grid.ts +6 -0
- package/src/markdown/markdown-highlight.ts +1 -0
- package/src/markdown/thread-code-syntax.ts +3 -0
- package/src/markdown/view-plan.ts +52 -0
- package/src/performance/perf-report.ts +1 -0
- package/src/performance/perf-timings.ts +1 -0
- package/src/prototype/components/PrototypeContentView.tsx +17 -0
- package/src/prototype/prototype-browser.ts +11 -0
- package/src/settings/components/SettingsDialog.tsx +3 -0
- package/src/settings/config.test.ts +79 -1
- package/src/settings/config.ts +91 -1
- package/src/settings/use-settings-dialog.test.tsx +172 -0
- package/src/settings/use-settings-dialog.tsx +115 -1
- package/src/settings/use-thread-harness-setting.test.tsx +87 -0
- package/src/settings/use-thread-harness-setting.ts +86 -0
- package/src/stories/stories.test.tsx +4 -0
- package/src/stories/story.ts +6 -0
- package/src/terminal/ansi-screen.ts +5 -0
- package/src/terminal/components/terminal-pane.ts +12 -0
- package/src/terminal/ghostty-terminal.ts +10 -0
- package/src/terminal/kitty-image.ts +3 -0
- package/src/terminal/pty.ts +9 -0
- package/src/testing/test-support.test.ts +2 -0
- package/src/testing/test-support.ts +16 -0
- package/src/thread/agent-continuation.test.ts +2 -0
- package/src/thread/agent-transcript.ts +7 -0
- package/src/thread/components/AgentConfigControls.test.tsx +41 -0
- package/src/thread/components/AgentConfigControls.tsx +8 -1
- package/src/thread/components/AgentThreadPane.test.tsx +91 -3
- package/src/thread/components/AgentThreadPane.tsx +61 -0
- package/src/thread/components/ThreadTree.tsx +1 -0
- package/src/thread/components/thread-tree.test.ts +1 -0
- package/src/thread/thread-controller.curate.test.ts +2 -0
- package/src/thread/thread-controller.share.test.ts +47 -2
- package/src/thread/thread-controller.ts +129 -4
- package/src/thread/tree-view.ts +4 -0
- package/src/thread/use-thread-agent.test.tsx +1 -0
- package/src/thread/use-thread-agent.ts +5 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +3 -0
- package/src/ui/components/OverlayScrollbar.tsx +5 -0
- package/src/ui/components/PromptDialog.tsx +2 -0
- package/src/ui/components/RootOverlay.tsx +1 -0
- package/src/ui/components/ScrollArea.tsx +2 -0
- package/src/ui/components/primitives/Button.tsx +1 -0
- package/src/ui/components/primitives/Tabs.tsx +1 -0
- package/src/ui/components/primitives/Tree.tsx +4 -0
- package/src/ui/components/primitives/tree-model.ts +7 -0
- package/src/ui/components/syntax-highlight.ts +5 -0
- package/src/ui/components/truncate-title.ts +1 -0
- package/src/ui/use-frame-measure.ts +3 -0
- package/src/ui/use-terminal-virtualizer.ts +6 -0
- package/src/workbench/components/ChangesColumn.tsx +7 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +3 -0
- package/src/workbench/components/EditorGrid.tsx +8 -0
- package/src/workbench/components/FileContentsView.tsx +2 -0
- package/src/workbench/components/GridTabContent.tsx +2 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
- package/src/workbench/components/ProjectTreeView.tsx +8 -0
- package/src/workbench/components/TreePane.tsx +2 -0
- package/src/workbench/components/editor-grid.ts +3 -0
- package/src/workbench/components/file-tree.ts +2 -0
- package/src/workbench/use-changes-workbench.ts +5 -0
- package/src/workbench/use-remember-layout.ts +2 -0
- package/src/workbench/use-repo-changes.ts +2 -0
package/src/keyboard/keymap.ts
CHANGED
|
@@ -121,9 +121,11 @@ export function reduceKey(state: KeyState, key: KeyInput, resolvedAction?: strin
|
|
|
121
121
|
const overlayGrammar = overlayGrammars[state.overlay];
|
|
122
122
|
|
|
123
123
|
if (overlayGrammar) return overlayGrammar(state, key);
|
|
124
|
+
|
|
124
125
|
const action = resolvedAction ?? actionFor(state.keys, name, key.shift);
|
|
125
126
|
|
|
126
127
|
if (action === "quit") return [{ type: "exit" }];
|
|
128
|
+
|
|
127
129
|
// the ONE read-only rule: any mutating attempt answers instead of acting
|
|
128
130
|
// (span-mode c, x, and a are hardwired keys, so they gate by name as well)
|
|
129
131
|
const mutating =
|
|
@@ -140,8 +142,10 @@ export function reduceKey(state: KeyState, key: KeyInput, resolvedAction?: strin
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
if (state.view === "inbox") return inboxGrammar(state, name);
|
|
145
|
+
|
|
143
146
|
// span mode owns its single-letter keys (b slides the span back)
|
|
144
147
|
if (state.spanMode) return spanGrammar(state, name);
|
|
148
|
+
|
|
145
149
|
if (state.view === "diff") return diffGrammar(state, action);
|
|
146
150
|
|
|
147
151
|
return planGrammar(state, action, name);
|
|
@@ -164,6 +168,7 @@ function composeOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
|
164
168
|
const name = key.name;
|
|
165
169
|
|
|
166
170
|
if (name === "escape") return [{ type: "closeOverlay" }];
|
|
171
|
+
|
|
167
172
|
if (name === "return" || name === "enter") {
|
|
168
173
|
// In the composer, ⌥/Alt+⏎ (meta) and shift+⏎ insert a newline - the
|
|
169
174
|
// focused textarea owns that; only a bare ⏎ saves. The submit overlay
|
|
@@ -180,6 +185,7 @@ function submitOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
|
180
185
|
const name = key.name;
|
|
181
186
|
|
|
182
187
|
if (name === "escape") return [{ type: "closeOverlay" }];
|
|
188
|
+
|
|
183
189
|
if (name === "left" || name === "right") {
|
|
184
190
|
return [{ type: "cycleMessage", direction: name === "left" ? -1 : 1 }];
|
|
185
191
|
}
|
|
@@ -191,6 +197,7 @@ function confirmOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
|
191
197
|
const name = key.name;
|
|
192
198
|
|
|
193
199
|
if (name === "return" || name === "enter") return [{ type: "confirmDialog" }];
|
|
200
|
+
|
|
194
201
|
if (name === "escape") return [{ type: "closeOverlay" }];
|
|
195
202
|
|
|
196
203
|
return [];
|
|
@@ -200,6 +207,7 @@ function promptOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
|
200
207
|
const name = key.name;
|
|
201
208
|
|
|
202
209
|
if (name === "return" || name === "enter") return [{ type: "confirmDialog" }];
|
|
210
|
+
|
|
203
211
|
if (name === "escape") return [{ type: "closeOverlay" }];
|
|
204
212
|
|
|
205
213
|
return [];
|
|
@@ -209,8 +217,11 @@ function spanActionsOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
|
209
217
|
const name = key.name;
|
|
210
218
|
|
|
211
219
|
if (name === "j" || name === "down") return [{ type: "moveSpanAction", direction: 1 }];
|
|
220
|
+
|
|
212
221
|
if (name === "k" || name === "up") return [{ type: "moveSpanAction", direction: -1 }];
|
|
222
|
+
|
|
213
223
|
if (name === "return" || name === "enter") return [{ type: "pickSpanAction" }];
|
|
224
|
+
|
|
214
225
|
if (name === "escape") return [{ type: "closeSpanActions" }];
|
|
215
226
|
|
|
216
227
|
return [];
|
|
@@ -220,7 +231,9 @@ function completionOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
|
220
231
|
const name = key.name;
|
|
221
232
|
|
|
222
233
|
if (name === "return" || name === "enter" || name === "q") return [{ type: "finishReview" }];
|
|
234
|
+
|
|
223
235
|
if (name === "a") return [{ type: "optInAutoClose" }];
|
|
236
|
+
|
|
224
237
|
if (name === "escape") return [{ type: "dismissCompletion" }];
|
|
225
238
|
|
|
226
239
|
return [];
|
|
@@ -230,11 +243,15 @@ function walkOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
|
230
243
|
const name = key.name;
|
|
231
244
|
|
|
232
245
|
if (name === "]") return [{ type: "walkForward" }];
|
|
246
|
+
|
|
233
247
|
if (name === "[") return [{ type: "walkBack" }];
|
|
248
|
+
|
|
234
249
|
if (name === "escape") return [{ type: "walkLeave" }];
|
|
250
|
+
|
|
235
251
|
if ((name === "return" || name === "enter") && state.walkAtEnd) {
|
|
236
252
|
return [{ type: "walkLeave" }, { type: "openSubmit" }];
|
|
237
253
|
}
|
|
254
|
+
|
|
238
255
|
if (name === "q") return [{ type: "exit" }];
|
|
239
256
|
|
|
240
257
|
return [];
|
|
@@ -242,9 +259,13 @@ function walkOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
|
242
259
|
|
|
243
260
|
function inboxGrammar(state: KeyState, name: string): Intent[] {
|
|
244
261
|
if (!state.hasInboxItems) return [];
|
|
262
|
+
|
|
245
263
|
if (name === "j" || name === "down") return [{ type: "inboxMove", to: "down" }];
|
|
264
|
+
|
|
246
265
|
if (name === "k" || name === "up") return [{ type: "inboxMove", to: "up" }];
|
|
266
|
+
|
|
247
267
|
if (name === "return" || name === "enter") return [{ type: "openSession" }];
|
|
268
|
+
|
|
248
269
|
if (name === "d") return [{ type: "requestDeleteSession" }];
|
|
249
270
|
|
|
250
271
|
return [];
|
|
@@ -254,39 +275,49 @@ function diffGrammar(state: KeyState, action: string | undefined): Intent[] {
|
|
|
254
275
|
const navigation = navigationIntent(action);
|
|
255
276
|
|
|
256
277
|
if (navigation) return navigation;
|
|
278
|
+
|
|
257
279
|
if (action === "walk") {
|
|
258
280
|
// marking viewed writes the session record, so a resolved review answers
|
|
259
281
|
if (state.resolved) return status("review submitted - read-only");
|
|
260
282
|
|
|
261
283
|
return [{ type: "walkStart" }];
|
|
262
284
|
}
|
|
285
|
+
|
|
263
286
|
if (action === "comment") {
|
|
264
287
|
if (state.resolved) return status("review submitted - read-only");
|
|
288
|
+
|
|
265
289
|
if (!state.cursorAnnotatable) return status("move to a code line to comment");
|
|
266
290
|
|
|
267
291
|
return [{ type: "openCompose", kind: "comment", from: "cursor" }];
|
|
268
292
|
}
|
|
293
|
+
|
|
269
294
|
// cut rejects the change under the cursor, reject_hunk the whole hunk; both
|
|
270
295
|
// write the working copy, so they gate on owner like a plan edit.
|
|
271
296
|
if (action === "cut" || action === "reject_hunk") {
|
|
272
297
|
if (state.resolved) return status("review submitted - read-only");
|
|
298
|
+
|
|
273
299
|
if (state.canEditPlan === false) return status("only the diff owner can curate hunks");
|
|
274
300
|
|
|
275
301
|
return [{ type: action === "reject_hunk" ? "rejectHunk" : "rejectChange" }];
|
|
276
302
|
}
|
|
303
|
+
|
|
277
304
|
const viewToggle = viewToggleIntent(action);
|
|
278
305
|
|
|
279
306
|
if (viewToggle) return viewToggle;
|
|
307
|
+
|
|
280
308
|
// restore un-does a curated-out rejection from the rail; same owner gate as reject
|
|
281
309
|
if (action === "restore_curation") {
|
|
282
310
|
if (state.resolved) return status("review submitted - read-only");
|
|
311
|
+
|
|
283
312
|
if (state.canEditPlan === false) return status("only the diff owner can curate hunks");
|
|
284
313
|
|
|
285
314
|
return [{ type: "restoreCuration" }];
|
|
286
315
|
}
|
|
316
|
+
|
|
287
317
|
const shared = annotationCluster(state, action);
|
|
288
318
|
|
|
289
319
|
if (shared) return shared;
|
|
320
|
+
|
|
290
321
|
if (action === "span" || action === "edit") {
|
|
291
322
|
return status("plan-only primitive - diff review uses c on a line");
|
|
292
323
|
}
|
|
@@ -296,13 +327,18 @@ function diffGrammar(state: KeyState, action: string | undefined): Intent[] {
|
|
|
296
327
|
|
|
297
328
|
function spanGrammar(state: KeyState, name: string): Intent[] {
|
|
298
329
|
if (name === "escape") return [{ type: "closeOverlay" }];
|
|
330
|
+
|
|
299
331
|
if (SPAN_KEYS.has(name)) return [{ type: "spanKey", name }];
|
|
332
|
+
|
|
300
333
|
// c comments, x cuts, a opens quick-actions - all mutate, so a resolved
|
|
301
334
|
// review is read-only, like the plan grammar
|
|
302
335
|
if (name === "c" || name === "x" || name === "a") {
|
|
303
336
|
if (state.resolved) return status("review submitted - read-only");
|
|
337
|
+
|
|
304
338
|
if (name === "c") return [{ type: "openCompose", kind: "comment", from: "span" }];
|
|
339
|
+
|
|
305
340
|
if (name === "a") return [{ type: "openSpanActions" }];
|
|
341
|
+
|
|
306
342
|
if (state.canEditPlan === false) return [];
|
|
307
343
|
|
|
308
344
|
return [{ type: "spanCut" }];
|
|
@@ -315,28 +351,38 @@ function planGrammar(state: KeyState, action: string | undefined, name: string):
|
|
|
315
351
|
const navigation = navigationIntent(action);
|
|
316
352
|
|
|
317
353
|
if (navigation) return navigation;
|
|
354
|
+
|
|
318
355
|
if (action === "walk") return status("the guided walk is a diff-review mode");
|
|
356
|
+
|
|
319
357
|
if (name === "escape") return [{ type: "deselect" }];
|
|
358
|
+
|
|
320
359
|
if (action === "span") return state.cursorAnnotatable ? [{ type: "startSpan" }] : [];
|
|
360
|
+
|
|
321
361
|
if (action === "comment") {
|
|
322
362
|
if (state.resolved) return status("review submitted - read-only");
|
|
363
|
+
|
|
323
364
|
if (!state.cursorAnnotatable) return status("text is cut - restore it first");
|
|
324
365
|
|
|
325
366
|
return [{ type: "openCompose", kind: "comment", from: "cursor" }];
|
|
326
367
|
}
|
|
368
|
+
|
|
327
369
|
// restore un-does a rail removal (a cut block); a plan edit, so owner-only
|
|
328
370
|
if (action === "restore_curation") {
|
|
329
371
|
if (state.resolved) return status("review submitted - read-only");
|
|
372
|
+
|
|
330
373
|
if (state.canEditPlan === false) return [];
|
|
331
374
|
|
|
332
375
|
return [{ type: "restoreCuration" }];
|
|
333
376
|
}
|
|
377
|
+
|
|
334
378
|
if (action === "cut" || action === "edit") {
|
|
335
379
|
if (state.resolved) return status("review submitted - read-only");
|
|
380
|
+
|
|
336
381
|
// the document selects, the rail edits: with a card selected, Cut deletes
|
|
337
382
|
// the annotation and edit rewrites the card body in place
|
|
338
383
|
if (state.hasFocusedAnnotation)
|
|
339
384
|
return [action === "cut" ? { type: "removeAnnotation" } : { type: "editCard" }];
|
|
385
|
+
|
|
340
386
|
// editing the plan itself is the owner's primitive; a share viewer only annotates,
|
|
341
387
|
// and has no edit affordance, so the key is silent rather than a nag
|
|
342
388
|
if (state.canEditPlan === false) return [];
|
|
@@ -360,7 +406,9 @@ function navigationIntent(action: string | undefined): Intent[] | null {
|
|
|
360
406
|
* split/stacked (split lays out only when the Changes pane is wide/zoomed). */
|
|
361
407
|
function viewToggleIntent(action: string | undefined): Intent[] | null {
|
|
362
408
|
if (action === "collapse_file") return [{ type: "foldFile" }];
|
|
409
|
+
|
|
363
410
|
if (action === "expand_file") return [{ type: "unfoldFile" }];
|
|
411
|
+
|
|
364
412
|
if (action === "split_diff") return [{ type: "toggleDiffView" }];
|
|
365
413
|
|
|
366
414
|
return null;
|
|
@@ -373,16 +421,19 @@ function annotationCluster(state: KeyState, action: string | undefined): Intent[
|
|
|
373
421
|
|
|
374
422
|
return [{ type: action === "next_annotation" ? "nextAnnotation" : "prevAnnotation" }];
|
|
375
423
|
}
|
|
424
|
+
|
|
376
425
|
if (action === "delete_annotation") {
|
|
377
426
|
if (state.resolved || !state.hasFocusedAnnotation) return [];
|
|
378
427
|
|
|
379
428
|
return [{ type: "removeAnnotation" }];
|
|
380
429
|
}
|
|
430
|
+
|
|
381
431
|
if (action === "rename") {
|
|
382
432
|
if (!state.hasFocusedAnnotation) return status("select a collaborator's note to rename them");
|
|
383
433
|
|
|
384
434
|
return [{ type: "openRename" }];
|
|
385
435
|
}
|
|
436
|
+
|
|
386
437
|
if (action === "submit") {
|
|
387
438
|
// a collaborator's notes union back as they go; there is no message to submit
|
|
388
439
|
if (state.canSubmitMessage === false)
|
package/src/keyboard/skills.ts
CHANGED
|
@@ -37,6 +37,7 @@ export function loadSkills(dir: string): SlashItem[] {
|
|
|
37
37
|
|
|
38
38
|
for (const entry of entries) {
|
|
39
39
|
if (!entry.isDirectory()) continue;
|
|
40
|
+
|
|
40
41
|
let text;
|
|
41
42
|
|
|
42
43
|
try {
|
|
@@ -47,6 +48,7 @@ export function loadSkills(dir: string): SlashItem[] {
|
|
|
47
48
|
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(text.replace(/\r\n/g, "\n"));
|
|
48
49
|
|
|
49
50
|
if (!frontmatter) continue;
|
|
51
|
+
|
|
50
52
|
const name = v.safeParse(
|
|
51
53
|
SkillNameSchema,
|
|
52
54
|
frontmatterValue(frontmatter[1]!, "name") ?? entry.name,
|
|
@@ -28,11 +28,14 @@ const WORD_BOUNDARY = /[\s\-_./:]/;
|
|
|
28
28
|
/** Order-preserving subsequence score, or null when a query character is missing; higher is better. */
|
|
29
29
|
export function scoreMatch(name: string, query: string): number | null {
|
|
30
30
|
if (query.length === 0) return 0;
|
|
31
|
+
|
|
31
32
|
const lowerName = name.toLowerCase();
|
|
32
33
|
const lowerQuery = query.toLowerCase();
|
|
33
34
|
|
|
34
35
|
if (lowerName === lowerQuery) return 1000;
|
|
36
|
+
|
|
35
37
|
if (lowerName.startsWith(lowerQuery)) return 500 - name.length;
|
|
38
|
+
|
|
36
39
|
let score = 0;
|
|
37
40
|
let cursor = 0;
|
|
38
41
|
let run = 0;
|
|
@@ -41,9 +44,12 @@ export function scoreMatch(name: string, query: string): number | null {
|
|
|
41
44
|
const found = lowerName.indexOf(character, cursor);
|
|
42
45
|
|
|
43
46
|
if (found === -1) return null;
|
|
47
|
+
|
|
44
48
|
run = found === cursor ? run + 1 : 0;
|
|
45
49
|
score += found === cursor ? 5 + run : 1;
|
|
50
|
+
|
|
46
51
|
if (found === 0 || WORD_BOUNDARY.test(lowerName[found - 1]!)) score += 10;
|
|
52
|
+
|
|
47
53
|
score -= found - cursor;
|
|
48
54
|
cursor = found + 1;
|
|
49
55
|
}
|
|
@@ -53,6 +59,7 @@ export function scoreMatch(name: string, query: string): number | null {
|
|
|
53
59
|
|
|
54
60
|
export function slashFilter(items: SlashItem[], query: string): SlashItem[] {
|
|
55
61
|
if (query.length === 0) return items;
|
|
62
|
+
|
|
56
63
|
const scored: Array<{ item: SlashItem; score: number }> = [];
|
|
57
64
|
|
|
58
65
|
for (const item of items) {
|
|
@@ -113,6 +120,7 @@ export function skillReferenceRanges(
|
|
|
113
120
|
const token = match[1]!;
|
|
114
121
|
|
|
115
122
|
if (!names.has(token.slice(1))) continue;
|
|
123
|
+
|
|
116
124
|
const start = match.index + match[0].length - token.length;
|
|
117
125
|
|
|
118
126
|
ranges.push({ start, end: start + token.length });
|
|
@@ -61,6 +61,7 @@ const RESOLVED: Intent = { type: "status", message: "review submitted - read-onl
|
|
|
61
61
|
|
|
62
62
|
function mutating(intent: Intent, context: ThreadNavContext): Intent {
|
|
63
63
|
if (!context.isOwner) return READ_ONLY;
|
|
64
|
+
|
|
64
65
|
if (context.resolved) return RESOLVED;
|
|
65
66
|
|
|
66
67
|
return intent;
|
|
@@ -134,13 +135,17 @@ export function resolveSessionChord(
|
|
|
134
135
|
): Intent | null {
|
|
135
136
|
if (key.name === "return" || key.name === "enter") {
|
|
136
137
|
if (!(key.ctrl || key.meta || key.super)) return null;
|
|
138
|
+
|
|
137
139
|
if (!context.isOwner) return READ_ONLY;
|
|
138
140
|
|
|
139
141
|
return context.resolved ? null : { type: "openSubmit" };
|
|
140
142
|
}
|
|
143
|
+
|
|
141
144
|
if (!key.ctrl) return null;
|
|
145
|
+
|
|
142
146
|
if (key.name === "e")
|
|
143
147
|
return context.resolved ? RESOLVED : context.isOwner ? { type: "edit" } : READ_ONLY;
|
|
148
|
+
|
|
144
149
|
if (key.name === "s") return context.isOwner ? { type: "share" } : READ_ONLY;
|
|
145
150
|
|
|
146
151
|
return null;
|
|
@@ -53,6 +53,7 @@ function newlinePrefixCounts(text: string): number[] {
|
|
|
53
53
|
|
|
54
54
|
for (let index = 0; index < text.length; index++) {
|
|
55
55
|
counts[index] = seen;
|
|
56
|
+
|
|
56
57
|
if (text[index] === "\n") seen += 1;
|
|
57
58
|
}
|
|
58
59
|
counts[text.length] = seen;
|
|
@@ -96,6 +97,7 @@ export const MarkdownThreadEditor = forwardRef<MarkdownEditorHandle, MarkdownThr
|
|
|
96
97
|
const editor = editorRef.current;
|
|
97
98
|
|
|
98
99
|
if (!editor) return;
|
|
100
|
+
|
|
99
101
|
editor.editBuffer.setSyntaxStyle(markdownEditorStyle(theme).style);
|
|
100
102
|
paintMarkdown(editor);
|
|
101
103
|
setPosition((prior) => ({ ...prior, lineCount: editor.editBuffer.getLineCount() }));
|
|
@@ -123,6 +125,7 @@ export const MarkdownThreadEditor = forwardRef<MarkdownEditorHandle, MarkdownThr
|
|
|
123
125
|
const editor = editorRef.current;
|
|
124
126
|
|
|
125
127
|
if (!editor) return;
|
|
128
|
+
|
|
126
129
|
paintMarkdown(editor);
|
|
127
130
|
setPosition((prior) => ({ ...prior, lineCount: editor.editBuffer.getLineCount() }));
|
|
128
131
|
}}
|
|
@@ -98,6 +98,7 @@ function highlightedByRow(): Map<number, string> {
|
|
|
98
98
|
|
|
99
99
|
if (backdrop || (caretCell && underlined)) marked += span.text;
|
|
100
100
|
}
|
|
101
|
+
|
|
101
102
|
if (marked.length > 0) rows.set(rowIndex, marked);
|
|
102
103
|
});
|
|
103
104
|
|
|
@@ -121,6 +122,7 @@ function caretCells(): Array<{ row: number; column: number; text: string }> {
|
|
|
121
122
|
if (red === CARET_RGB[0] && green === CARET_RGB[1] && blue === CARET_RGB[2]) {
|
|
122
123
|
cells.push({ row, column, text: span.text });
|
|
123
124
|
}
|
|
125
|
+
|
|
124
126
|
column += span.width;
|
|
125
127
|
}
|
|
126
128
|
});
|
|
@@ -158,6 +160,7 @@ async function framesUntilHighlight(expected: string, limit = 4): Promise<number
|
|
|
158
160
|
// one scheduler turn (the input parser and React commit), then one frame
|
|
159
161
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
160
162
|
await setup.renderOnce();
|
|
163
|
+
|
|
161
164
|
if (highlightedText() === expected) return frame;
|
|
162
165
|
}
|
|
163
166
|
|
|
@@ -103,7 +103,9 @@ function blockStyle(block: DisplayBlock, tokens: Theme): BlockStyle {
|
|
|
103
103
|
/** The read-only color of an inline markdown role: links accent, code and removals dim, insertions green. */
|
|
104
104
|
function roleForeground(role: RunRole, baseFg: string, tokens: Theme): string {
|
|
105
105
|
if (role === "link") return tokens.blue;
|
|
106
|
+
|
|
106
107
|
if (role === "ins") return tokens.green;
|
|
108
|
+
|
|
107
109
|
if (role === "code" || role === "del") return tokens.textDim;
|
|
108
110
|
|
|
109
111
|
return baseFg;
|
|
@@ -112,7 +114,9 @@ function roleForeground(role: RunRole, baseFg: string, tokens: Theme): string {
|
|
|
112
114
|
/** The text attributes an inline role adds over a block's base attributes. */
|
|
113
115
|
function roleAttributes(role: RunRole, baseAttributes: number): number {
|
|
114
116
|
if (role === "strong") return baseAttributes | BOLD;
|
|
117
|
+
|
|
115
118
|
if (role === "em") return baseAttributes | ITALIC;
|
|
119
|
+
|
|
116
120
|
if (role === "strike" || role === "del") return baseAttributes | CUT;
|
|
117
121
|
|
|
118
122
|
return baseAttributes;
|
|
@@ -288,6 +292,7 @@ export function ThreadView({
|
|
|
288
292
|
const block = display[index];
|
|
289
293
|
|
|
290
294
|
if (!block) return 1;
|
|
295
|
+
|
|
291
296
|
const usable = viewWidth > 0 ? Math.max(1, viewWidth - 6) : 40;
|
|
292
297
|
const lines = Math.max(1, Math.ceil(displayText(block).length / usable));
|
|
293
298
|
|
|
@@ -378,6 +383,7 @@ export function ThreadView({
|
|
|
378
383
|
// measurement never adds a row and shifts the blocks below it out from under a pending click
|
|
379
384
|
const headingRule = (block: DisplayBlock): React.ReactNode => {
|
|
380
385
|
if (block.kind !== "h1" && block.kind !== "h2") return null;
|
|
386
|
+
|
|
381
387
|
const width = Math.max(1, (viewWidth > 6 ? viewWidth : 46) - 6);
|
|
382
388
|
|
|
383
389
|
return (
|
|
@@ -398,6 +404,7 @@ export function ThreadView({
|
|
|
398
404
|
{custom}
|
|
399
405
|
</box>
|
|
400
406
|
);
|
|
407
|
+
|
|
401
408
|
const block = display[blockIndex]!;
|
|
402
409
|
const { baseFg, baseAttributes, marker } = blockStyle(block, tokens);
|
|
403
410
|
// list items of one list stay tight; every other block sits a blank row below its neighbour
|
|
@@ -472,8 +479,11 @@ export function ThreadView({
|
|
|
472
479
|
cards.length > 0,
|
|
473
480
|
),
|
|
474
481
|
);
|
|
482
|
+
|
|
475
483
|
if (isLastLine) lineRows.push(headingRule(block));
|
|
484
|
+
|
|
476
485
|
lineRows.push(...cards);
|
|
486
|
+
|
|
477
487
|
if (cards.length > 0 && !isLastLine) {
|
|
478
488
|
lineRows.push(<box key={`gap-${lineIndex}`} style={{ height: 1 }} />);
|
|
479
489
|
}
|
|
@@ -508,14 +518,17 @@ export function ThreadView({
|
|
|
508
518
|
for (let index = 0; index < display.length; index++) {
|
|
509
519
|
nodes.push(blockNodeFor(index));
|
|
510
520
|
mountedRows += estimateBlockRows(index);
|
|
521
|
+
|
|
511
522
|
if (mountedRows > terminalHeight + OVERSCAN_BLOCKS) break;
|
|
512
523
|
}
|
|
513
524
|
|
|
514
525
|
return nodes;
|
|
515
526
|
}
|
|
527
|
+
|
|
516
528
|
if (firstItem.start > 0) {
|
|
517
529
|
nodes.push(<box key="spacer-above" style={{ height: firstItem.start }} />);
|
|
518
530
|
}
|
|
531
|
+
|
|
519
532
|
for (const item of virtual.items) nodes.push(blockNodeFor(item.index));
|
|
520
533
|
const below = virtual.totalSize - lastItem.end;
|
|
521
534
|
|
|
@@ -36,6 +36,7 @@ export function runsFor(text: string, ranges: MarkRange[]): Run[] {
|
|
|
36
36
|
const end = edges[index + 1]!;
|
|
37
37
|
|
|
38
38
|
if (end <= start) continue;
|
|
39
|
+
|
|
39
40
|
const covering = ranges.filter((range) => range.start <= start && end <= range.end);
|
|
40
41
|
|
|
41
42
|
// the caret cell shows through a mark, so the head of a selection is visible
|
|
@@ -84,6 +85,7 @@ export function wrapLines(text: string, width: number): VisualLine[] {
|
|
|
84
85
|
lines.push({ start: lineStart, end: lineEnd });
|
|
85
86
|
lineStart = wordStart;
|
|
86
87
|
}
|
|
88
|
+
|
|
87
89
|
// a token wider than the line (a long identifier, a URL) breaks at the cell edge, so
|
|
88
90
|
// every visual line stays one terminal row and nothing is clipped away
|
|
89
91
|
while (wordEnd - lineStart > width) {
|
|
@@ -94,6 +96,7 @@ export function wrapLines(text: string, width: number): VisualLine[] {
|
|
|
94
96
|
}
|
|
95
97
|
lines.push({ start: lineStart, end: Math.max(lineEnd, lineStart) });
|
|
96
98
|
}
|
|
99
|
+
|
|
97
100
|
segmentStart = segmentEnd + 1;
|
|
98
101
|
}
|
|
99
102
|
|
|
@@ -114,6 +117,7 @@ export function lineMarkRanges(ranges: MarkRange[], line: VisualLine): MarkRange
|
|
|
114
117
|
/** Printable input that should reach a composer: one key, or a whole pasted run in one event. */
|
|
115
118
|
export function printableSequence(key: KeyEvent): string | null {
|
|
116
119
|
if (!key.sequence || key.ctrl || key.meta) return null;
|
|
120
|
+
|
|
117
121
|
// reject any run carrying a control byte, so escape sequences and \r\t\b never seed a draft
|
|
118
122
|
for (const character of key.sequence) if (character < " ") return null;
|
|
119
123
|
|
|
@@ -32,6 +32,7 @@ export function markdownEditorStyle(theme: Theme): MarkdownEditorStyle {
|
|
|
32
32
|
const cached = markdownEditorStyleCache.get(theme);
|
|
33
33
|
|
|
34
34
|
if (cached) return cached;
|
|
35
|
+
|
|
35
36
|
const groupStyles = markdownGroupStyles(theme);
|
|
36
37
|
const style = SyntaxStyle.fromStyles(groupStyles);
|
|
37
38
|
const styleIds = new Map<MarkdownHighlightGroup, number>();
|
|
@@ -52,7 +52,9 @@ function splitTableRow(line: string): string[] {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
parts.push(cell);
|
|
55
|
+
|
|
55
56
|
if (parts.length > 1 && parts[0] === "") parts.shift();
|
|
57
|
+
|
|
56
58
|
if (parts.length > 1 && parts[parts.length - 1] === "") parts.pop();
|
|
57
59
|
|
|
58
60
|
return parts.map((part) => part.trim());
|
|
@@ -78,6 +80,7 @@ function pad(text: string, width: number, align: ColumnAlign): string {
|
|
|
78
80
|
const slack = Math.max(0, width - text.length);
|
|
79
81
|
|
|
80
82
|
if (align === "right") return " ".repeat(slack) + text;
|
|
83
|
+
|
|
81
84
|
if (align === "center") {
|
|
82
85
|
const left = Math.floor(slack / 2);
|
|
83
86
|
|
|
@@ -132,6 +135,7 @@ export function parseFrontmatterRows(text: string): FrontmatterRow[] {
|
|
|
132
135
|
|
|
133
136
|
for (const line of text.split("\n")) {
|
|
134
137
|
if (line.trim() === "") continue;
|
|
138
|
+
|
|
135
139
|
const match = /^([^:\s][^:]*):\s?(.*)$/.exec(line);
|
|
136
140
|
|
|
137
141
|
if (match) {
|
|
@@ -163,6 +167,7 @@ function wrapCell(value: string, width: number): string[] {
|
|
|
163
167
|
*/
|
|
164
168
|
export function layoutFrontmatterGrid(rows: FrontmatterRow[], maxWidth: number): GridLine[] {
|
|
165
169
|
if (rows.length === 0) return [];
|
|
170
|
+
|
|
166
171
|
const keyWidth = Math.max(3, ...rows.map((row) => row.key.length));
|
|
167
172
|
// size the value column to its content, capped at the width left after the key column and borders,
|
|
168
173
|
// so a short frontmatter table stays compact and a long value wraps instead of overflowing
|
|
@@ -183,6 +188,7 @@ export function layoutFrontmatterGrid(rows: FrontmatterRow[], maxWidth: number):
|
|
|
183
188
|
|
|
184
189
|
rows.forEach((row, rowIndex) => {
|
|
185
190
|
if (rowIndex > 0) lines.push(rule("├", "┼", "┤"));
|
|
191
|
+
|
|
186
192
|
const valueLines = wrapCell(row.value, valueWidth);
|
|
187
193
|
|
|
188
194
|
valueLines.forEach((valueLine, valueIndex) => {
|
|
@@ -50,6 +50,7 @@ function scanInlineTokens(text: string, base: number, into: MarkdownHighlightRan
|
|
|
50
50
|
into.push({ start, end, group: "code" });
|
|
51
51
|
continue;
|
|
52
52
|
}
|
|
53
|
+
|
|
53
54
|
// a link colors its brackets and href; the [] label stays plain unless it is an @-scope, matching how a
|
|
54
55
|
// markdown source view paints links
|
|
55
56
|
const closeBracket = token.indexOf("]");
|
|
@@ -14,6 +14,7 @@ export async function highlightThreadCodeBlocks(
|
|
|
14
14
|
): Promise<Map<number, SyntaxSpan[]>> {
|
|
15
15
|
const jobs = display.flatMap((block, blockIndex) => {
|
|
16
16
|
if (block.kind !== "code") return [];
|
|
17
|
+
|
|
17
18
|
const filetype = filetypeFor(block.work?.lang ?? block.base?.lang);
|
|
18
19
|
|
|
19
20
|
return filetype ? [{ blockIndex, filetype, source: renderedText(block) }] : [];
|
|
@@ -21,6 +22,7 @@ export async function highlightThreadCodeBlocks(
|
|
|
21
22
|
const byBlock = new Map<number, SyntaxSpan[]>();
|
|
22
23
|
|
|
23
24
|
if (jobs.length === 0) return byBlock;
|
|
25
|
+
|
|
24
26
|
const client = getTreeSitterClient();
|
|
25
27
|
|
|
26
28
|
await client.initialize();
|
|
@@ -28,6 +30,7 @@ export async function highlightThreadCodeBlocks(
|
|
|
28
30
|
const result = await client.highlightOnce(job.source, job.filetype);
|
|
29
31
|
|
|
30
32
|
if (!result.highlights) continue;
|
|
33
|
+
|
|
31
34
|
const spans: SyntaxSpan[] = [];
|
|
32
35
|
let offset = 0;
|
|
33
36
|
const lines = job.source.split("\n");
|