@cueloop/client 0.1.0-alpha.22 → 0.1.0-alpha.23
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.plan-review.test.tsx +19 -0
- package/src/App.tsx +17 -12
- package/src/components/PlanSheet.stories.tsx +1 -1
- package/src/components/PlanSheet.tsx +13 -6
- package/src/key-bindings.test.ts +11 -0
- package/src/key-bindings.ts +11 -1
- package/src/keymap.test.ts +3 -3
- package/src/keymap.ts +3 -2
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.23",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.ts"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
11
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
12
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
13
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
10
|
+
"@cueloop/daemon": "0.1.0-alpha.23",
|
|
11
|
+
"@cueloop/extension-api": "0.1.0-alpha.23",
|
|
12
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.23",
|
|
13
|
+
"@cueloop/schema": "0.1.0-alpha.23",
|
|
14
14
|
"@opentui/core": "^0.5.1",
|
|
15
15
|
"@opentui/keymap": "0.5.1",
|
|
16
16
|
"@opentui/react": "^0.5.1",
|
|
@@ -85,6 +85,25 @@ describe("share button", () => {
|
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
describe("edit affordance", () => {
|
|
89
|
+
test("the owner sees the Edit button", async () => {
|
|
90
|
+
// Arrange / Act
|
|
91
|
+
const owner = await renderApp();
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
94
|
+
expect(owner.captureCharFrame()).toContain("Edit");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a read-only viewer (a plan shared over ssh) never sees the Edit button", async () => {
|
|
98
|
+
// Arrange / Act
|
|
99
|
+
const viewer = await testRender(<App home={home} sessionId={session.id} readOnly />, { width: 120, height: 32 });
|
|
100
|
+
await waitForText(viewer, "cueloop");
|
|
101
|
+
|
|
102
|
+
// Assert
|
|
103
|
+
expect(viewer.captureCharFrame()).not.toContain("Edit");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
88
107
|
describe("selection feeds the annotation quote", () => {
|
|
89
108
|
test("v anchors, l extends, c composes with the char-precise quote", async () => {
|
|
90
109
|
// Arrange
|
package/src/App.tsx
CHANGED
|
@@ -74,9 +74,10 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
74
74
|
// the plan or a verdict. `observer` is what the controller and every write
|
|
75
75
|
// gate key off; the two capability flags carve out the collaborator's middle.
|
|
76
76
|
const observer = readOnly || role === "observer";
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
// Editing the plan, submitting a verdict, and sharing are all the owner's
|
|
78
|
+
// alone - a collaborator annotates, an observer only reads. One predicate
|
|
79
|
+
// feeds all three; split it the day a collaborator earns one of them.
|
|
80
|
+
const isOwner = !observer && role === "owner";
|
|
80
81
|
const controller = useMemo(
|
|
81
82
|
() => createReviewController({ home, sessionId, readOnly: observer, onExit, clock, openClient }),
|
|
82
83
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -263,9 +264,9 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
263
264
|
const state: KeyState = {
|
|
264
265
|
keys: keysRef.current,
|
|
265
266
|
readOnly: observer,
|
|
266
|
-
canEditPlan,
|
|
267
|
-
canSubmitVerdict,
|
|
268
|
-
canShare,
|
|
267
|
+
canEditPlan: isOwner,
|
|
268
|
+
canSubmitVerdict: isOwner,
|
|
269
|
+
canShare: isOwner,
|
|
269
270
|
overlay,
|
|
270
271
|
view: !session ? "inbox" : isDiff ? "diff" : "plan",
|
|
271
272
|
spanMode: mode.type === "span",
|
|
@@ -358,22 +359,23 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
358
359
|
};
|
|
359
360
|
|
|
360
361
|
const onEditRequest = (): void => {
|
|
361
|
-
|
|
362
|
-
|
|
362
|
+
// A share viewer/observer has no Edit affordance (the button is hidden), so
|
|
363
|
+
// this is owner-only; stay silent rather than nag if it is ever reached.
|
|
364
|
+
if (!isOwner) return;
|
|
363
365
|
if (resolved) return controller.setStatus("review submitted - read-only");
|
|
364
366
|
runEditorHandOff();
|
|
365
367
|
};
|
|
366
368
|
|
|
367
369
|
// clicking the rail Share button: publish the plan, copy the ssh line
|
|
368
370
|
const onShareRequest = (): void => {
|
|
369
|
-
if (!
|
|
371
|
+
if (!isOwner) return controller.setStatus("only the plan owner can share");
|
|
370
372
|
controller.share();
|
|
371
373
|
};
|
|
372
374
|
|
|
373
375
|
// clicking the rail Submit button: same read-only answer as the submit key
|
|
374
376
|
const onSubmitRequest = (): void => {
|
|
375
377
|
if (observer) return controller.setStatus("observer - read-only");
|
|
376
|
-
if (!
|
|
378
|
+
if (!isOwner) return controller.setStatus("shared view - your notes save as you go");
|
|
377
379
|
if (resolved) return;
|
|
378
380
|
dispatch({ type: "openSubmit" });
|
|
379
381
|
};
|
|
@@ -448,7 +450,9 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
448
450
|
? "walk"
|
|
449
451
|
: focusedAnnotationId !== undefined
|
|
450
452
|
? "card"
|
|
451
|
-
: "
|
|
453
|
+
: role === "collaborator"
|
|
454
|
+
? "collaborator"
|
|
455
|
+
: "normal";
|
|
452
456
|
|
|
453
457
|
return (
|
|
454
458
|
<ThemeProvider theme={theme}>
|
|
@@ -489,6 +493,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
489
493
|
editOrphanCount={editOrphanCount}
|
|
490
494
|
onLineActivate={onLineActivate}
|
|
491
495
|
onEditRequest={onEditRequest}
|
|
496
|
+
canEdit={isOwner}
|
|
492
497
|
/>
|
|
493
498
|
)}
|
|
494
499
|
<ReviewPanel
|
|
@@ -511,7 +516,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
511
516
|
onSelectCard: selectCardFromRail,
|
|
512
517
|
onActivateCard: openCardEdit,
|
|
513
518
|
onSubmitRequest,
|
|
514
|
-
onShareRequest:
|
|
519
|
+
onShareRequest: isOwner ? onShareRequest : undefined,
|
|
515
520
|
}}
|
|
516
521
|
/>
|
|
517
522
|
</box>
|
|
@@ -6,7 +6,7 @@ import { fixtureDisplay, fixtureMarks, fixturePlanSession } from "./story-fixtur
|
|
|
6
6
|
|
|
7
7
|
export const meta: StoryMeta = { title: "PlanSheet" };
|
|
8
8
|
|
|
9
|
-
const callbacks = { onLineActivate: () => {}, onEditRequest: () => {} };
|
|
9
|
+
const callbacks = { onLineActivate: () => {}, onEditRequest: () => {}, canEdit: true };
|
|
10
10
|
|
|
11
11
|
export const AnnotatedPlan: Story = {
|
|
12
12
|
render: () => (
|
|
@@ -64,6 +64,8 @@ export interface PlanSheetProps {
|
|
|
64
64
|
editOrphanCount: number;
|
|
65
65
|
onLineActivate: (displayIndex: number) => void;
|
|
66
66
|
onEditRequest: () => void;
|
|
67
|
+
/** Owner-only: show the Edit affordance. A share viewer never sees it. */
|
|
68
|
+
canEdit: boolean;
|
|
67
69
|
theme?: Theme;
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -97,6 +99,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
97
99
|
editOrphanCount,
|
|
98
100
|
onLineActivate,
|
|
99
101
|
onEditRequest,
|
|
102
|
+
canEdit,
|
|
100
103
|
theme,
|
|
101
104
|
}: PlanSheetProps,
|
|
102
105
|
handleRef,
|
|
@@ -215,7 +218,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
215
218
|
|
|
216
219
|
return (
|
|
217
220
|
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
218
|
-
<SheetHeader session={session} onEditRequest={onEditRequest} theme={theme} />
|
|
221
|
+
<SheetHeader session={session} onEditRequest={onEditRequest} canEdit={canEdit} theme={theme} />
|
|
219
222
|
{editOrphanCount > 0 ? (
|
|
220
223
|
<box style={{ height: 1, backgroundColor: tokens.markCommentBg, paddingLeft: 2 }}>
|
|
221
224
|
<text fg={tokens.red}>
|
|
@@ -234,10 +237,12 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
234
237
|
function SheetHeader({
|
|
235
238
|
session,
|
|
236
239
|
onEditRequest,
|
|
240
|
+
canEdit,
|
|
237
241
|
theme,
|
|
238
242
|
}: {
|
|
239
243
|
session: ReviewSession;
|
|
240
244
|
onEditRequest: () => void;
|
|
245
|
+
canEdit: boolean;
|
|
241
246
|
theme?: Theme;
|
|
242
247
|
}): React.ReactNode {
|
|
243
248
|
const tokens = useComponentTheme(theme);
|
|
@@ -255,11 +260,13 @@ function SheetHeader({
|
|
|
255
260
|
) : null}
|
|
256
261
|
</text>
|
|
257
262
|
<box style={{ flexGrow: 1 }} />
|
|
258
|
-
|
|
259
|
-
<
|
|
260
|
-
{
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
{canEdit ? (
|
|
264
|
+
<Toolbar>
|
|
265
|
+
<Button onPress={onEditRequest} theme={theme}>
|
|
266
|
+
{" Edit "}
|
|
267
|
+
</Button>
|
|
268
|
+
</Toolbar>
|
|
269
|
+
) : null}
|
|
263
270
|
</box>
|
|
264
271
|
);
|
|
265
272
|
}
|
package/src/key-bindings.test.ts
CHANGED
|
@@ -118,6 +118,17 @@ describe("getActiveKeys-generated status hints", () => {
|
|
|
118
118
|
expect(bindings().statusHint("read-only")).toBe("observer - read-only · j/k move · n/p annotations · q quit");
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
test("collaborator hint annotates but shows no plan edit or submit", () => {
|
|
122
|
+
// Arrange / Act
|
|
123
|
+
const hint = bindings().statusHint("collaborator");
|
|
124
|
+
|
|
125
|
+
// Assert
|
|
126
|
+
expect(hint).toBe("j/k move · v span · drag selects · c comment · s suggest · n/p annotations · q quit");
|
|
127
|
+
expect(hint).not.toContain("cut");
|
|
128
|
+
expect(hint).not.toContain("edit");
|
|
129
|
+
expect(hint).not.toContain("submit");
|
|
130
|
+
});
|
|
131
|
+
|
|
121
132
|
test("a rebound key shows its real binding in the hint", () => {
|
|
122
133
|
// Arrange
|
|
123
134
|
const resolver = new KeyBindings({ ...DEFAULT_KEYS, comment: ["m"] });
|
package/src/key-bindings.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface KeyLayerContext {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/** Hint templates keyed by the same view states the status line shows. */
|
|
27
|
-
export type HintMode = "normal" | "card" | "span" | "compose" | "submit" | "walk" | "read-only";
|
|
27
|
+
export type HintMode = "normal" | "collaborator" | "card" | "span" | "compose" | "submit" | "walk" | "read-only";
|
|
28
28
|
|
|
29
29
|
type HintEntry =
|
|
30
30
|
| { text: string }
|
|
@@ -43,6 +43,16 @@ const HINT_TEMPLATES: Record<HintMode, HintEntry[]> = {
|
|
|
43
43
|
{ commands: ["submit"], label: "submit" },
|
|
44
44
|
{ commands: ["quit"], label: "quit" },
|
|
45
45
|
],
|
|
46
|
+
// A share viewer: annotate and navigate, no plan edit / verdict submit.
|
|
47
|
+
collaborator: [
|
|
48
|
+
{ commands: ["down", "up"], label: "move" },
|
|
49
|
+
{ commands: ["span"], label: "span" },
|
|
50
|
+
{ text: "drag selects" },
|
|
51
|
+
{ commands: ["comment"], label: "comment" },
|
|
52
|
+
{ commands: ["suggest"], label: "suggest" },
|
|
53
|
+
{ commands: ["next_annotation", "prev_annotation"], label: "annotations" },
|
|
54
|
+
{ commands: ["quit"], label: "quit" },
|
|
55
|
+
],
|
|
46
56
|
card: [
|
|
47
57
|
{ text: "card" },
|
|
48
58
|
{ commands: ["edit"], label: "edit" },
|
package/src/keymap.test.ts
CHANGED
|
@@ -144,13 +144,13 @@ describe("collaborator (share) capabilities", () => {
|
|
|
144
144
|
expect(reduceKey(collab, key("e"), "edit")).toEqual([{ type: "editCard" }]);
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
test("cannot edit the plan: cut/edit with no card is
|
|
147
|
+
test("cannot edit the plan: cut/edit with no card is silent (no nag, no button)", () => {
|
|
148
148
|
// Arrange
|
|
149
149
|
const collab = state({ canEditPlan: false, hasFocusedAnnotation: false });
|
|
150
150
|
|
|
151
151
|
// Act / Assert
|
|
152
|
-
expect(reduceKey(collab, key("e"), "edit")).toEqual([
|
|
153
|
-
expect(reduceKey(collab, key("c"), "cut")).toEqual([
|
|
152
|
+
expect(reduceKey(collab, key("e"), "edit")).toEqual([]);
|
|
153
|
+
expect(reduceKey(collab, key("c"), "cut")).toEqual([]);
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
test("cannot submit a verdict: there is no agent on a share", () => {
|
package/src/keymap.ts
CHANGED
|
@@ -217,8 +217,9 @@ function planGrammar(state: KeyState, action: string | undefined, name: string):
|
|
|
217
217
|
// the document selects, the rail edits: with a card selected, Cut deletes
|
|
218
218
|
// the annotation and edit rewrites the card body in place
|
|
219
219
|
if (state.hasFocusedAnnotation) return [action === "cut" ? { type: "removeAnnotation" } : { type: "editCard" }];
|
|
220
|
-
// editing the plan itself is the owner's verb
|
|
221
|
-
|
|
220
|
+
// editing the plan itself is the owner's verb; a share viewer only annotates,
|
|
221
|
+
// and has no edit affordance, so the key is silent rather than a nag
|
|
222
|
+
if (state.canEditPlan === false) return [];
|
|
222
223
|
return [{ type: action }];
|
|
223
224
|
}
|
|
224
225
|
return annotationCluster(state, action) ?? [];
|