@cueloop/client 0.1.0-alpha.79 → 0.1.0-alpha.80
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/App.diff.test.tsx +2 -2
- package/src/App.export.test.tsx +3 -3
- package/src/App.plan-review.test.tsx +1 -1
- package/src/App.readonly.test.tsx +2 -2
- package/src/App.submit-confirm.test.tsx +7 -7
- package/src/App.test.tsx +11 -11
- package/src/App.tsx +16 -11
- package/src/App.walk.test.tsx +5 -5
- package/src/app-key-state.ts +1 -1
- package/src/app-screens.tsx +5 -5
- package/src/app-view-model.ts +5 -5
- package/src/components/AnnotatableFileView.stories.tsx +1 -1
- package/src/components/AnnotatableFileView.test.tsx +1 -1
- package/src/components/BareWorkbenchFileView.tsx +1 -1
- package/src/components/Breadcrumb.stories.tsx +1 -1
- package/src/components/ChangesColumn.stories.tsx +6 -1
- package/src/components/CompletionOverlay.stories.tsx +2 -2
- package/src/components/CompletionOverlay.tsx +4 -4
- package/src/components/ConfirmCard.stories.tsx +4 -4
- package/src/components/ConfirmCard.tsx +31 -32
- package/src/components/DiffContentView.tsx +1 -1
- package/src/components/KeybindsDialog.stories.tsx +1 -1
- package/src/components/ThreadFooter.test.tsx +12 -1
- package/src/components/ThreadFooter.tsx +7 -2
- package/src/components/ThreadTree.stories.tsx +1 -1
- package/src/components/ThreadTree.tsx +1 -1
- package/src/components/ThreadView.tsx +1 -1
- package/src/components/WelcomePlayground.tsx +1 -1
- package/src/components/__snapshots__/stories.test.tsx.snap +8 -8
- package/src/components/story-fixtures.ts +2 -2
- package/src/components/{session-tree.test.ts → thread-tree.test.ts} +2 -2
- package/src/config.test.ts +10 -10
- package/src/config.ts +6 -9
- package/src/integrations.test.ts +8 -8
- package/src/integrations.ts +4 -4
- package/src/intent-dispatch.test.ts +10 -10
- package/src/intent-dispatch.ts +15 -14
- package/src/key-bindings.test.ts +4 -4
- package/src/key-bindings.ts +10 -10
- package/src/keymap.test.ts +5 -5
- package/src/keymap.ts +7 -7
- package/src/thread-controller.curate.test.ts +15 -5
- package/src/thread-controller.derivation.test.ts +1 -1
- package/src/thread-controller.live-diff.test.ts +10 -10
- package/src/thread-controller.optimistic.test.ts +1 -1
- package/src/thread-controller.share.test.ts +8 -8
- package/src/thread-controller.switch-cache.test.ts +4 -4
- package/src/thread-controller.tree.test.ts +4 -4
- package/src/thread-controller.ts +12 -12
- package/src/tree-view.test.ts +1 -1
- package/src/tree-view.ts +3 -3
- package/src/use-annotation-surface.tsx +1 -1
- package/src/view-diff.test.ts +1 -1
- package/src/workbench-snapshot.test.ts +1 -1
- /package/src/components/{session-tree.ts → thread-tree.ts} +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Submit button expanded into a bordered confirm card at the rail bottom:
|
|
3
|
-
* the
|
|
3
|
+
* the message selector (arrow keys or click), the optional summary, and plain
|
|
4
4
|
* word-buttons - key hints live in the status line only. The bordered height
|
|
5
5
|
* derives from the content rows through Card, so layout and render never drift.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import React, { useContext } from "react";
|
|
9
9
|
import { useTerminalDimensions } from "@opentui/react";
|
|
10
|
-
import type {
|
|
10
|
+
import type { MessageOutcome } from "@cueloop/schema";
|
|
11
11
|
import type { Theme } from "../theme";
|
|
12
12
|
import type { QuickAction } from "../config";
|
|
13
13
|
import { SlashSkillsContext } from "../skills";
|
|
@@ -21,17 +21,16 @@ import { DialogActions } from "./primitives/DialogActions";
|
|
|
21
21
|
const SUBMIT_CARD_MAX_WIDTH = 48;
|
|
22
22
|
const PALETTE_WINDOW = 5;
|
|
23
23
|
|
|
24
|
-
export const
|
|
24
|
+
export const MESSAGE_OUTCOMES: MessageOutcome[] = ["approved", "changes_requested"];
|
|
25
25
|
|
|
26
|
-
/** Selector words in the confirm card - one word per
|
|
27
|
-
export const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
request_changes: "Changes",
|
|
26
|
+
/** Selector words in the confirm card - one word per message. */
|
|
27
|
+
export const MESSAGE_OUTCOME_LABEL: Record<MessageOutcome, string> = {
|
|
28
|
+
approved: "Approve",
|
|
29
|
+
changes_requested: "Changes",
|
|
31
30
|
};
|
|
32
31
|
|
|
33
32
|
export interface ConfirmCardProps {
|
|
34
|
-
|
|
33
|
+
message: MessageOutcome;
|
|
35
34
|
summary: string;
|
|
36
35
|
/**
|
|
37
36
|
* The guided walk's honest coverage line for diff sessions, e.g.
|
|
@@ -39,53 +38,53 @@ export interface ConfirmCardProps {
|
|
|
39
38
|
*/
|
|
40
39
|
viewedSummary?: string;
|
|
41
40
|
onInput: (summary: string) => void;
|
|
42
|
-
|
|
41
|
+
onSelectMessage: (message: MessageOutcome) => void;
|
|
43
42
|
onSubmit: () => void;
|
|
44
43
|
onCancel: () => void;
|
|
45
44
|
quickActions: QuickAction[];
|
|
46
45
|
theme?: Theme;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
/** 1-row
|
|
48
|
+
/** 1-row message selector, spacer, summary input, spacer, buttons. */
|
|
50
49
|
const CONFIRM_CONTENT_ROWS = 5;
|
|
51
50
|
|
|
52
|
-
export function
|
|
53
|
-
return
|
|
51
|
+
export function outcomeColor(message: MessageOutcome, tokens: Theme): string {
|
|
52
|
+
return message === "approved"
|
|
54
53
|
? tokens.green
|
|
55
|
-
:
|
|
54
|
+
: message === "changes_requested"
|
|
56
55
|
? tokens.red
|
|
57
56
|
: tokens.blue;
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
/**
|
|
61
|
-
* The
|
|
62
|
-
* direction of a choice between
|
|
63
|
-
* the grammar (←/→ cycle the
|
|
64
|
-
* selected
|
|
60
|
+
* The message selector: one row of pressable words, matching the reading
|
|
61
|
+
* direction of a choice between two outcomes. Selection stays controlled by
|
|
62
|
+
* the grammar (←/→ cycle the message); a click selects directly. The
|
|
63
|
+
* selected message wears brackets and its color.
|
|
65
64
|
*/
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
function MessageSelector({
|
|
66
|
+
message,
|
|
67
|
+
onSelectMessage,
|
|
69
68
|
theme,
|
|
70
69
|
}: {
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
message: MessageOutcome;
|
|
71
|
+
onSelectMessage: (message: MessageOutcome) => void;
|
|
73
72
|
theme?: Theme;
|
|
74
73
|
}): React.ReactNode {
|
|
75
74
|
const tokens = useComponentTheme(theme);
|
|
76
75
|
|
|
77
76
|
return (
|
|
78
77
|
<box style={{ flexDirection: "row", height: 1, width: "100%", justifyContent: "center" }}>
|
|
79
|
-
{
|
|
78
|
+
{MESSAGE_OUTCOMES.map((candidate) => (
|
|
80
79
|
<box
|
|
81
80
|
key={candidate}
|
|
82
81
|
style={{ paddingRight: 1 }}
|
|
83
|
-
onMouseUp={() =>
|
|
82
|
+
onMouseUp={() => onSelectMessage(candidate)}
|
|
84
83
|
>
|
|
85
|
-
<text fg={candidate ===
|
|
86
|
-
{candidate ===
|
|
87
|
-
? `[${
|
|
88
|
-
: ` ${
|
|
84
|
+
<text fg={candidate === message ? outcomeColor(candidate, tokens) : tokens.textDim}>
|
|
85
|
+
{candidate === message
|
|
86
|
+
? `[${MESSAGE_OUTCOME_LABEL[candidate]}]`
|
|
87
|
+
: ` ${MESSAGE_OUTCOME_LABEL[candidate]} `}
|
|
89
88
|
</text>
|
|
90
89
|
</box>
|
|
91
90
|
))}
|
|
@@ -94,11 +93,11 @@ function VerdictSelector({
|
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
export function ConfirmCard({
|
|
97
|
-
|
|
96
|
+
message,
|
|
98
97
|
summary,
|
|
99
98
|
viewedSummary,
|
|
100
99
|
onInput,
|
|
101
|
-
|
|
100
|
+
onSelectMessage,
|
|
102
101
|
onSubmit,
|
|
103
102
|
onCancel,
|
|
104
103
|
quickActions,
|
|
@@ -133,7 +132,7 @@ export function ConfirmCard({
|
|
|
133
132
|
>
|
|
134
133
|
{viewedSummary !== undefined ? <text fg={tokens.textDim}>{viewedSummary}</text> : null}
|
|
135
134
|
{viewedSummary !== undefined ? <box style={{ height: 1 }} /> : null}
|
|
136
|
-
<
|
|
135
|
+
<MessageSelector message={message} onSelectMessage={onSelectMessage} theme={theme} />
|
|
137
136
|
<box style={{ height: 1 }} />
|
|
138
137
|
<SlashComposer
|
|
139
138
|
seed={summary}
|
|
@@ -79,7 +79,7 @@ export interface DiffContentViewProps {
|
|
|
79
79
|
observer: boolean;
|
|
80
80
|
/** Whether comments can be drafted here; false for a non-diff thread's view-only live diff. */
|
|
81
81
|
commentsEnabled?: boolean;
|
|
82
|
-
/** A
|
|
82
|
+
/** A message is in: no draft may open; the app answers with its read-only status. */
|
|
83
83
|
resolved?: boolean;
|
|
84
84
|
/** True while a menu, dialog, or overlay owns the keyboard. */
|
|
85
85
|
suspended?: boolean;
|
|
@@ -27,10 +27,21 @@ test("a long branch truncates on one line and keeps the send control in place",
|
|
|
27
27
|
const frame = setup.captureCharFrame();
|
|
28
28
|
const footerRow = frame.split("\n").find((line) => line.includes("cueloop"))!;
|
|
29
29
|
// the repo/branch context and the send control share the single footer row
|
|
30
|
-
expect(footerRow).toContain("
|
|
30
|
+
expect(footerRow).toContain("Send message (0)");
|
|
31
31
|
// the branch is clipped to an ellipsis, not wrapped onto a second row
|
|
32
32
|
expect(footerRow).toContain("…");
|
|
33
33
|
expect(frame).not.toContain("long-name");
|
|
34
34
|
|
|
35
35
|
setup.renderer.destroy();
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
test("the footer shows the open annotation count", async () => {
|
|
39
|
+
const setup = await testRender(
|
|
40
|
+
<ThreadFooter repo="cueloop" branch="main" pendingAnnotations={3} theme={DARK} />,
|
|
41
|
+
{ width: 60, height: 3 },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await settle(setup);
|
|
45
|
+
expect(setup.captureCharFrame()).toContain("Send message (3)");
|
|
46
|
+
setup.renderer.destroy();
|
|
47
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The thread's bottom band: a header-like bar under the thread column, or the
|
|
2
2
|
// Changes column when a zoom drops the thread column. It carries the repo/branch
|
|
3
|
-
// context on the left and a written, clickable "Send message" control on the right.
|
|
3
|
+
// context on the left and a written, clickable "Send message (n)" control on the right.
|
|
4
4
|
|
|
5
5
|
import React, { useRef } from "react";
|
|
6
6
|
import type { BoxRenderable } from "@opentui/core";
|
|
@@ -15,6 +15,8 @@ export interface ThreadFooterProps {
|
|
|
15
15
|
onSubmit?: () => void;
|
|
16
16
|
/** When false the send control dims and does not fire (e.g. an observer). */
|
|
17
17
|
canSubmit?: boolean;
|
|
18
|
+
/** Open reviewer annotations included in the next Message. */
|
|
19
|
+
pendingAnnotations?: number;
|
|
18
20
|
theme?: Theme;
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -29,6 +31,7 @@ export function ThreadFooter({
|
|
|
29
31
|
branch,
|
|
30
32
|
onSubmit,
|
|
31
33
|
canSubmit = true,
|
|
34
|
+
pendingAnnotations = 0,
|
|
32
35
|
theme,
|
|
33
36
|
}: ThreadFooterProps): React.ReactNode {
|
|
34
37
|
const tokens = theme ?? DARK;
|
|
@@ -64,7 +67,9 @@ export function ThreadFooter({
|
|
|
64
67
|
</box>
|
|
65
68
|
<box style={{ flexGrow: 1 }} />
|
|
66
69
|
<box onMouseUp={canSubmit ? onSubmit : undefined} style={{ flexShrink: 0 }}>
|
|
67
|
-
<text fg={canSubmit ? tokens.accent : tokens.textDim}>
|
|
70
|
+
<text fg={canSubmit ? tokens.accent : tokens.textDim}>
|
|
71
|
+
{`Send message (${pendingAnnotations})`}
|
|
72
|
+
</text>
|
|
68
73
|
</box>
|
|
69
74
|
</box>
|
|
70
75
|
);
|
|
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { DARK } from "../theme";
|
|
3
3
|
import type { Story, StoryMeta } from "./story";
|
|
4
4
|
import { ThreadTree } from "./ThreadTree";
|
|
5
|
-
import { groupInbox } from "./
|
|
5
|
+
import { groupInbox } from "./thread-tree";
|
|
6
6
|
import { fixtureDiffSession, fixturePlanSession } from "./story-fixtures";
|
|
7
7
|
|
|
8
8
|
export const meta: StoryMeta = { title: "Surfaces/ThreadTree" };
|
|
@@ -17,7 +17,7 @@ import { useMenuControl } from "./menu-control";
|
|
|
17
17
|
import { IconButton } from "./primitives/IconButton";
|
|
18
18
|
import { NERD } from "./primitives/icons";
|
|
19
19
|
import { truncateTitle } from "./truncate-title";
|
|
20
|
-
import { threadTitle, type InboxRow } from "./
|
|
20
|
+
import { threadTitle, type InboxRow } from "./thread-tree";
|
|
21
21
|
|
|
22
22
|
export interface ThreadTreeProps {
|
|
23
23
|
rows: InboxRow[];
|
|
@@ -143,7 +143,7 @@ export interface ThreadViewProps {
|
|
|
143
143
|
editOrphanCount?: number;
|
|
144
144
|
/** Reports whether a composer is open, so session chords can yield to typing. */
|
|
145
145
|
onComposingChange?: (composing: boolean) => void;
|
|
146
|
-
/** A
|
|
146
|
+
/** A message is in: no draft may open; the app answers with its read-only status. */
|
|
147
147
|
resolved?: boolean;
|
|
148
148
|
/** An observer or a resolved review refused a draft; the app shows why. */
|
|
149
149
|
onObserverBlocked?: (reason: "observer" | "resolved") => void;
|
|
@@ -151,7 +151,7 @@ exports[`stories catalog Surfaces/BareWorkbenchFileView/Loaded renders, snapshot
|
|
|
151
151
|
`;
|
|
152
152
|
|
|
153
153
|
exports[`stories catalog Chrome/Breadcrumb/SessionHeader renders, snapshots, and carries its colors: Chrome/Breadcrumb/SessionHeader 1`] = `
|
|
154
|
-
" cueloop · Migration Plan · rev 2 · resolved:
|
|
154
|
+
" cueloop · Migration Plan · rev 2 · resolved: approved
|
|
155
155
|
|
|
156
156
|
|
|
157
157
|
|
|
@@ -185,7 +185,7 @@ exports[`stories catalog Surfaces/ChangesColumn/Changed renders, snapshots, and
|
|
|
185
185
|
│ components
|
|
186
186
|
│ ChangesColumn.tsx A
|
|
187
187
|
│ daemon/src
|
|
188
|
-
│ review.ts
|
|
188
|
+
│ thread-review.ts D
|
|
189
189
|
│ README.md M
|
|
190
190
|
│
|
|
191
191
|
│
|
|
@@ -364,7 +364,7 @@ exports[`stories catalog Overlays/CompletionOverlay/FeedbackCounting renders, sn
|
|
|
364
364
|
|
|
365
365
|
exports[`stories catalog Cards/ConfirmCard/ApproveDefault renders, snapshots, and carries its colors: Cards/ConfirmCard/ApproveDefault 1`] = `
|
|
366
366
|
"┌────────────────────────────────┐
|
|
367
|
-
│
|
|
367
|
+
│ [Approve] Changes │
|
|
368
368
|
│ │
|
|
369
369
|
│ summary for the agent ( │
|
|
370
370
|
│ │
|
|
@@ -382,7 +382,7 @@ exports[`stories catalog Cards/ConfirmCard/ApproveDefault renders, snapshots, an
|
|
|
382
382
|
|
|
383
383
|
exports[`stories catalog Cards/ConfirmCard/RequestChanges renders, snapshots, and carries its colors: Cards/ConfirmCard/RequestChanges 1`] = `
|
|
384
384
|
"┌────────────────────────────────┐
|
|
385
|
-
│
|
|
385
|
+
│ Approve [Changes] │
|
|
386
386
|
│ │
|
|
387
387
|
│ Tighten the steps. │
|
|
388
388
|
│ │
|
|
@@ -402,7 +402,7 @@ exports[`stories catalog Cards/ConfirmCard/WithViewedSummary renders, snapshots,
|
|
|
402
402
|
"┌────────────────────────────────┐
|
|
403
403
|
│ 2/3 files viewed │
|
|
404
404
|
│ │
|
|
405
|
-
│
|
|
405
|
+
│ [Approve] Changes │
|
|
406
406
|
│ │
|
|
407
407
|
│ summary for the agent ( │
|
|
408
408
|
│ │
|
|
@@ -689,7 +689,7 @@ exports[`stories catalog Overlays/KeybindsDialog/Cheatsheet renders, snapshots,
|
|
|
689
689
|
│ enter submit │
|
|
690
690
|
│ │
|
|
691
691
|
│ Submit │
|
|
692
|
-
│ ← / →
|
|
692
|
+
│ ← / → message │
|
|
693
693
|
│ esc cancel │
|
|
694
694
|
│ │
|
|
695
695
|
│ [esc] │
|
|
@@ -1270,13 +1270,13 @@ exports[`stories catalog Chrome/SlashComposer/WithDraft renders, snapshots, and
|
|
|
1270
1270
|
|
|
1271
1271
|
exports[`stories catalog Chrome/ThreadFooter/Default renders, snapshots, and carries its colors: Chrome/ThreadFooter/Default 1`] = `
|
|
1272
1272
|
"────────────────────────────────────────────────────────────────────────────────
|
|
1273
|
-
cueloop / main
|
|
1273
|
+
cueloop / main Send message (0)
|
|
1274
1274
|
"
|
|
1275
1275
|
`;
|
|
1276
1276
|
|
|
1277
1277
|
exports[`stories catalog Chrome/ThreadFooter/ReadOnly renders, snapshots, and carries its colors: Chrome/ThreadFooter/ReadOnly 1`] = `
|
|
1278
1278
|
"────────────────────────────────────────────────────────────────────────────────
|
|
1279
|
-
cueloop / design/tui
|
|
1279
|
+
cueloop / design/tui Send message (0)
|
|
1280
1280
|
"
|
|
1281
1281
|
`;
|
|
1282
1282
|
|
|
@@ -63,7 +63,7 @@ export function fixturePlanSession(overrides: Partial<Thread> = {}): Thread {
|
|
|
63
63
|
},
|
|
64
64
|
revisions: [{ revision: 1, content: FIXTURE_PLAN, submittedAt: "2026-01-01T00:00:00Z" }],
|
|
65
65
|
annotations: FIXTURE_ANNOTATIONS,
|
|
66
|
-
|
|
66
|
+
message: null,
|
|
67
67
|
status: "pending",
|
|
68
68
|
createdAt: "2026-01-01T00:00:00Z",
|
|
69
69
|
...overrides,
|
|
@@ -82,7 +82,7 @@ export function fixtureDiffSession(overrides: Partial<Thread> = {}): Thread {
|
|
|
82
82
|
},
|
|
83
83
|
revisions: [{ revision: 1, content: FIXTURE_PATCH, submittedAt: "2026-01-01T00:00:00Z" }],
|
|
84
84
|
annotations: [],
|
|
85
|
-
|
|
85
|
+
message: null,
|
|
86
86
|
status: "pending",
|
|
87
87
|
createdAt: "2026-01-01T00:00:00Z",
|
|
88
88
|
...overrides,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type { Thread, WorkspaceKey } from "@cueloop/schema";
|
|
3
|
-
import { groupInbox, projectName } from "./
|
|
3
|
+
import { groupInbox, projectName } from "./thread-tree";
|
|
4
4
|
|
|
5
5
|
function session(id: string, title: string, rootCommit?: string, remote?: string): Thread {
|
|
6
6
|
const workspace: WorkspaceKey = { repoRoot: `/home/dev/${id}-checkout`, branch: "main" };
|
|
@@ -14,7 +14,7 @@ function session(id: string, title: string, rootCommit?: string, remote?: string
|
|
|
14
14
|
artifact: { type: "plan", content: `# ${title}\n`, meta: { title } },
|
|
15
15
|
revisions: [],
|
|
16
16
|
annotations: [],
|
|
17
|
-
|
|
17
|
+
message: null,
|
|
18
18
|
status: "pending",
|
|
19
19
|
createdAt: "2026-01-01T00:00:00.000Z",
|
|
20
20
|
};
|
package/src/config.test.ts
CHANGED
|
@@ -143,19 +143,19 @@ describe("loadConfig", () => {
|
|
|
143
143
|
}
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
-
test("[ui]
|
|
146
|
+
test("[ui] default_message defaults to approve and parses changes_requested", () => {
|
|
147
147
|
// Arrange
|
|
148
|
-
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-
|
|
148
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-message-"));
|
|
149
149
|
const path = join(dir, "config.toml");
|
|
150
150
|
|
|
151
|
-
writeFileSync(path, `[ui]\
|
|
151
|
+
writeFileSync(path, `[ui]\ndefault_message = "changes_requested"\n`);
|
|
152
152
|
|
|
153
153
|
try {
|
|
154
154
|
// Assert
|
|
155
|
-
expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.
|
|
156
|
-
"
|
|
155
|
+
expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.defaultMessage).toBe(
|
|
156
|
+
"approved",
|
|
157
157
|
);
|
|
158
|
-
expect(loadConfig({ userConfigPath: path }).ui.
|
|
158
|
+
expect(loadConfig({ userConfigPath: path }).ui.defaultMessage).toBe("changes_requested");
|
|
159
159
|
} finally {
|
|
160
160
|
rmSync(dir, { recursive: true, force: true });
|
|
161
161
|
}
|
|
@@ -393,7 +393,7 @@ describe("integrations.obsidian config", () => {
|
|
|
393
393
|
|
|
394
394
|
writeFileSync(
|
|
395
395
|
path,
|
|
396
|
-
`[integrations.obsidian]\nvault = "/notes/vault"\nfolder = "plans"\nexportOn = "
|
|
396
|
+
`[integrations.obsidian]\nvault = "/notes/vault"\nfolder = "plans"\nexportOn = "approved"\nseparator = "comma"\n`,
|
|
397
397
|
);
|
|
398
398
|
|
|
399
399
|
try {
|
|
@@ -403,7 +403,7 @@ describe("integrations.obsidian config", () => {
|
|
|
403
403
|
// Assert
|
|
404
404
|
expect(config.integrations.obsidian.vault).toBe("/notes/vault");
|
|
405
405
|
expect(config.integrations.obsidian.folder).toBe("plans");
|
|
406
|
-
expect(config.integrations.obsidian.exportOn).toBe("
|
|
406
|
+
expect(config.integrations.obsidian.exportOn).toBe("approved");
|
|
407
407
|
expect(config.integrations.obsidian.separator).toBe("space"); // invalid value falls back
|
|
408
408
|
expect(config.keys["comment"]).toEqual(["c"]); // other sections untouched
|
|
409
409
|
} finally {
|
|
@@ -417,7 +417,7 @@ describe("integrations.obsidian config", () => {
|
|
|
417
417
|
const user = join(dir, "user.toml");
|
|
418
418
|
const repoRoot = join(dir, "repo");
|
|
419
419
|
|
|
420
|
-
writeFileSync(user, `[integrations.obsidian]\nvault = "/user/vault"\nexportOn = "
|
|
420
|
+
writeFileSync(user, `[integrations.obsidian]\nvault = "/user/vault"\nexportOn = "message"\n`);
|
|
421
421
|
Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
|
|
422
422
|
writeFileSync(
|
|
423
423
|
join(repoRoot, ".cueloop", "config.toml"),
|
|
@@ -430,7 +430,7 @@ describe("integrations.obsidian config", () => {
|
|
|
430
430
|
|
|
431
431
|
// Assert
|
|
432
432
|
expect(config.integrations.obsidian.vault).toBe("/user/vault"); // user layer survives
|
|
433
|
-
expect(config.integrations.obsidian.exportOn).toBe("
|
|
433
|
+
expect(config.integrations.obsidian.exportOn).toBe("message");
|
|
434
434
|
expect(config.integrations.obsidian.folder).toBe("repo-plans"); // repo layer wins
|
|
435
435
|
} finally {
|
|
436
436
|
rmSync(dir, { recursive: true, force: true });
|
package/src/config.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { OBSIDIAN_DEFAULTS, type ObsidianConfig } from "@cueloop/integration-obsidian";
|
|
12
|
-
import type {
|
|
12
|
+
import type { MessageOutcome } from "@cueloop/schema";
|
|
13
13
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
14
14
|
import { dirname } from "node:path";
|
|
15
15
|
import * as v from "valibot";
|
|
@@ -108,7 +108,7 @@ export interface CueloopConfig {
|
|
|
108
108
|
theme: ThemeName;
|
|
109
109
|
/** How the Changes diff renders when wide: old|new side by side or one stacked column. */
|
|
110
110
|
diffView: DiffViewMode;
|
|
111
|
-
|
|
111
|
+
defaultMessage: MessageOutcome;
|
|
112
112
|
/** Session ids the user has pinned to the top of the sidebar; client-local view state. */
|
|
113
113
|
pins: string[];
|
|
114
114
|
/** The last pane layout a bare launch restores; unset until the user changes one. */
|
|
@@ -213,10 +213,7 @@ const UiSchema = v.object({
|
|
|
213
213
|
editor: v.fallback(v.optional(v.string()), undefined),
|
|
214
214
|
theme: v.fallback(v.optional(v.string()), undefined),
|
|
215
215
|
diff_view: v.fallback(v.optional(v.picklist(["split", "stacked", "unified"])), undefined),
|
|
216
|
-
|
|
217
|
-
v.optional(v.picklist(["comment", "approve", "request_changes"])),
|
|
218
|
-
undefined,
|
|
219
|
-
),
|
|
216
|
+
default_message: v.fallback(v.optional(v.picklist(["approved", "changes_requested"])), undefined),
|
|
220
217
|
pins: v.fallback(v.optional(v.array(v.string())), undefined),
|
|
221
218
|
layout: v.fallback(
|
|
222
219
|
v.optional(
|
|
@@ -234,7 +231,7 @@ const ObsidianSchema = v.object({
|
|
|
234
231
|
folder: v.fallback(v.optional(v.string()), undefined),
|
|
235
232
|
filenameFormat: v.fallback(v.optional(v.string()), undefined),
|
|
236
233
|
separator: v.fallback(v.optional(v.picklist(["space", "dash", "underscore"])), undefined),
|
|
237
|
-
exportOn: v.fallback(v.optional(v.picklist(["
|
|
234
|
+
exportOn: v.fallback(v.optional(v.picklist(["approved", "message", "manual"])), undefined),
|
|
238
235
|
});
|
|
239
236
|
const IntegrationsSchema = v.object({ obsidian: v.optional(ObsidianSchema) });
|
|
240
237
|
const ExperimentalSchema = v.object({
|
|
@@ -284,7 +281,7 @@ function applyUi(ui: CueloopConfig["ui"], parsed: v.InferOutput<typeof UiSchema>
|
|
|
284
281
|
// "unified" is the pre-rename spelling of "stacked"; keep loading it so an upgrade never flips the layout
|
|
285
282
|
if (parsed.diff_view !== undefined)
|
|
286
283
|
ui.diffView = parsed.diff_view === "unified" ? "stacked" : parsed.diff_view;
|
|
287
|
-
if (parsed.
|
|
284
|
+
if (parsed.default_message !== undefined) ui.defaultMessage = parsed.default_message;
|
|
288
285
|
if (parsed.pins !== undefined) ui.pins = parsed.pins;
|
|
289
286
|
if (parsed.layout !== undefined) {
|
|
290
287
|
ui.layout = {
|
|
@@ -364,7 +361,7 @@ export function loadConfig(
|
|
|
364
361
|
autoClose: "off",
|
|
365
362
|
theme: DEFAULT_THEME_NAME,
|
|
366
363
|
diffView: "split",
|
|
367
|
-
|
|
364
|
+
defaultMessage: "approved",
|
|
368
365
|
pins: [],
|
|
369
366
|
},
|
|
370
367
|
authors: {},
|
package/src/integrations.test.ts
CHANGED
|
@@ -18,24 +18,24 @@ describe("loadBundledExporters", () => {
|
|
|
18
18
|
|
|
19
19
|
test("runsOn mirrors the configured export policy", async () => {
|
|
20
20
|
// Arrange
|
|
21
|
-
const onApprove = (await loadBundledExporters(integrations({ exportOn: "
|
|
21
|
+
const onApprove = (await loadBundledExporters(integrations({ exportOn: "approved" })))[0]!;
|
|
22
22
|
|
|
23
23
|
// Assert
|
|
24
|
-
expect(onApprove.runsOn("
|
|
25
|
-
expect(onApprove.runsOn("
|
|
24
|
+
expect(onApprove.runsOn("approved")).toBeTrue();
|
|
25
|
+
expect(onApprove.runsOn("changes_requested")).toBeFalse();
|
|
26
26
|
|
|
27
27
|
// Arrange
|
|
28
|
-
const
|
|
28
|
+
const onMessage = (await loadBundledExporters(integrations({ exportOn: "message" })))[0]!;
|
|
29
29
|
|
|
30
30
|
// Assert
|
|
31
|
-
expect(
|
|
32
|
-
expect(
|
|
31
|
+
expect(onMessage.runsOn("approved")).toBeTrue();
|
|
32
|
+
expect(onMessage.runsOn("changes_requested")).toBeTrue();
|
|
33
33
|
|
|
34
34
|
// Arrange
|
|
35
35
|
const manual = (await loadBundledExporters(integrations({ exportOn: "manual" })))[0]!;
|
|
36
36
|
|
|
37
37
|
// Assert
|
|
38
|
-
expect(manual.runsOn("
|
|
39
|
-
expect(manual.runsOn("
|
|
38
|
+
expect(manual.runsOn("approved")).toBeFalse();
|
|
39
|
+
expect(manual.runsOn("changes_requested")).toBeFalse();
|
|
40
40
|
});
|
|
41
41
|
});
|
package/src/integrations.ts
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import { Registry, type Exporter } from "@cueloop/extension-api";
|
|
9
9
|
import { createObsidianExtension, shouldExport } from "@cueloop/integration-obsidian";
|
|
10
|
-
import type {
|
|
10
|
+
import type { MessageOutcome } from "@cueloop/schema";
|
|
11
11
|
import type { IntegrationsConfig } from "./config";
|
|
12
12
|
|
|
13
|
-
/** An exporter plus the policy deciding which
|
|
13
|
+
/** An exporter plus the policy deciding which messages trigger it. */
|
|
14
14
|
export interface BundledExporter {
|
|
15
15
|
name: string;
|
|
16
16
|
run: Exporter;
|
|
17
|
-
runsOn(
|
|
17
|
+
runsOn(message: MessageOutcome): boolean;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/** Load the bundled integrations from config as generic exporters. */
|
|
@@ -28,5 +28,5 @@ export async function loadBundledExporters(
|
|
|
28
28
|
|
|
29
29
|
if (!run) return [];
|
|
30
30
|
|
|
31
|
-
return [{ name: "obsidian", run, runsOn: (
|
|
31
|
+
return [{ name: "obsidian", run, runsOn: (message) => shouldExport(obsidian.exportOn, message) }];
|
|
32
32
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The intent reducer in isolation: with the App component's dependencies
|
|
3
3
|
* mocked, each Intent maps to exactly the controller call or state update it
|
|
4
|
-
* should - the navigation clamps, the annotation wrap-around, the
|
|
4
|
+
* should - the navigation clamps, the annotation wrap-around, the message
|
|
5
5
|
* cycle, and the submit-reveal fix, none of which needed a rendered TUI.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -36,7 +36,7 @@ function sessionWith(annotations: Annotation[], overrides: Partial<Thread> = {})
|
|
|
36
36
|
artifact: { type: "plan", content: "# Plan\n", meta: {} },
|
|
37
37
|
revisions: [],
|
|
38
38
|
annotations,
|
|
39
|
-
|
|
39
|
+
message: null,
|
|
40
40
|
status: "pending",
|
|
41
41
|
createdAt: "2026-01-01T00:00:00.000Z",
|
|
42
42
|
...overrides,
|
|
@@ -143,7 +143,7 @@ function makeDeps(overrides: Partial<IntentDispatchDeps> = {}): IntentDispatchDe
|
|
|
143
143
|
inboxCursor: 0,
|
|
144
144
|
mode: { type: "normal" },
|
|
145
145
|
session: null,
|
|
146
|
-
|
|
146
|
+
defaultMessage: "approved",
|
|
147
147
|
focusedAnnotationId: undefined,
|
|
148
148
|
selectedCurationId: undefined,
|
|
149
149
|
railTab: "review",
|
|
@@ -255,19 +255,19 @@ describe("annotation navigation", () => {
|
|
|
255
255
|
});
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
-
describe("
|
|
259
|
-
test("advances through the
|
|
258
|
+
describe("cycleMessage", () => {
|
|
259
|
+
test("advances through the message list in the submit overlay", () => {
|
|
260
260
|
// Arrange
|
|
261
|
-
const deps = makeDeps({ mode: { type: "submit",
|
|
261
|
+
const deps = makeDeps({ mode: { type: "submit", message: "approved", summary: "" } });
|
|
262
262
|
const dispatch = createIntentDispatch(deps);
|
|
263
263
|
|
|
264
264
|
// Act
|
|
265
|
-
dispatch({ type: "
|
|
265
|
+
dispatch({ type: "cycleMessage", direction: 1 });
|
|
266
266
|
|
|
267
267
|
// Assert
|
|
268
268
|
expect(deps.setMode).toHaveBeenCalledWith({
|
|
269
269
|
type: "submit",
|
|
270
|
-
|
|
270
|
+
message: "changes_requested",
|
|
271
271
|
summary: "",
|
|
272
272
|
});
|
|
273
273
|
});
|
|
@@ -368,7 +368,7 @@ describe("marker-actions popover", () => {
|
|
|
368
368
|
});
|
|
369
369
|
|
|
370
370
|
describe("openSubmit", () => {
|
|
371
|
-
test("opens the submit confirm with the default
|
|
371
|
+
test("opens the submit confirm with the default message", () => {
|
|
372
372
|
// Arrange
|
|
373
373
|
const deps = makeDeps({ session: sessionWith([]) });
|
|
374
374
|
const dispatch = createIntentDispatch(deps);
|
|
@@ -377,7 +377,7 @@ describe("openSubmit", () => {
|
|
|
377
377
|
dispatch({ type: "openSubmit" });
|
|
378
378
|
|
|
379
379
|
// Assert
|
|
380
|
-
expect(deps.setMode).toHaveBeenCalledWith({ type: "submit",
|
|
380
|
+
expect(deps.setMode).toHaveBeenCalledWith({ type: "submit", message: "approved", summary: "" });
|
|
381
381
|
});
|
|
382
382
|
|
|
383
383
|
test("does nothing without a session", () => {
|