@cueloop/client 0.1.0-alpha.70 → 0.1.0-alpha.72

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.70",
3
+ "version": "0.1.0-alpha.72",
4
4
  "description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
5
5
  "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
6
  "bugs": {
@@ -27,10 +27,10 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@cueloop/daemon": "0.1.0-alpha.70",
31
- "@cueloop/extension-api": "0.1.0-alpha.70",
32
- "@cueloop/integration-obsidian": "0.1.0-alpha.70",
33
- "@cueloop/schema": "0.1.0-alpha.70",
30
+ "@cueloop/daemon": "0.1.0-alpha.72",
31
+ "@cueloop/extension-api": "0.1.0-alpha.72",
32
+ "@cueloop/integration-obsidian": "0.1.0-alpha.72",
33
+ "@cueloop/schema": "0.1.0-alpha.72",
34
34
  "@opentui/core": "^0.5.8",
35
35
  "@opentui/keymap": "0.5.8",
36
36
  "@opentui/react": "^0.5.8",
package/src/App.tsx CHANGED
@@ -21,8 +21,8 @@ import {
21
21
  type DiffViewMode,
22
22
  type QuickAction,
23
23
  } from "./config";
24
- import { loadSkills, SlashSkillsContext } from "./skills";
25
- import type { SlashItem } from "./slash-palette";
24
+ import { loadSkills, PaletteNamesContext, SlashSkillsContext } from "./skills";
25
+ import { mergeSlashItems, slashItemsFrom, type SlashItem } from "./slash-palette";
26
26
  import {
27
27
  composeTheme,
28
28
  DEFAULT_THEME_NAME,
@@ -490,6 +490,10 @@ export function App({
490
490
  const [authorNames, setAuthorNames] = useState<Record<string, string>>({});
491
491
  const [quickActions, setQuickActions] = useState<QuickAction[]>(DEFAULT_QUICK_ACTIONS);
492
492
  const [skills, setSkills] = useState<SlashItem[]>([]);
493
+ const paletteNames = useMemo(
494
+ () => new Set(mergeSlashItems(slashItemsFrom(quickActions), skills).map((item) => item.name)),
495
+ [quickActions, skills],
496
+ );
493
497
  // opt-in: render a prototype as a kitty pixel mockup instead of the markdown doc
494
498
  const [prototypePixels, setPrototypePixels] = useState(false);
495
499
 
@@ -792,23 +796,25 @@ export function App({
792
796
  if (!session)
793
797
  return inbox ? (
794
798
  <SlashSkillsContext.Provider value={skills}>
795
- <NoThreadShell
796
- rows={grouped.rows}
797
- inboxCursor={inboxCursor}
798
- mode={mode}
799
- theme={theme}
800
- controller={controller}
801
- setMode={setMode}
802
- menuChrome={menuChrome}
803
- onOpenMenu={() => setMenuDialog("settings")}
804
- sidebarOpen={sidebarOpen}
805
- onToggleSidebar={() => setSidebarOpen((open) => !open)}
806
- pinnedIds={pinnedIds}
807
- onPin={togglePin}
808
- onRename={(id, title) => setMode({ type: "renameThread", sessionId: id, text: title })}
809
- quickActions={quickActions}
810
- onWelcomeComposingChange={setWelcomeComposing}
811
- />
799
+ <PaletteNamesContext.Provider value={paletteNames}>
800
+ <NoThreadShell
801
+ rows={grouped.rows}
802
+ inboxCursor={inboxCursor}
803
+ mode={mode}
804
+ theme={theme}
805
+ controller={controller}
806
+ setMode={setMode}
807
+ menuChrome={menuChrome}
808
+ onOpenMenu={() => setMenuDialog("settings")}
809
+ sidebarOpen={sidebarOpen}
810
+ onToggleSidebar={() => setSidebarOpen((open) => !open)}
811
+ pinnedIds={pinnedIds}
812
+ onPin={togglePin}
813
+ onRename={(id, title) => setMode({ type: "renameThread", sessionId: id, text: title })}
814
+ quickActions={quickActions}
815
+ onWelcomeComposingChange={setWelcomeComposing}
816
+ />
817
+ </PaletteNamesContext.Provider>
812
818
  </SlashSkillsContext.Provider>
813
819
  ) : (
814
820
  <ConnectingScreen theme={theme} />
@@ -878,228 +884,237 @@ export function App({
878
884
  dispatch({ type: "openSubmit" });
879
885
  };
880
886
 
887
+ const threadFooter = (
888
+ <ThreadFooter
889
+ repo={projectName(activeSession.workspace)}
890
+ branch={activeSession.workspace.branch}
891
+ onSubmit={onSubmitRequest}
892
+ canSubmit={canSubmitReview(isOwner, resolved, observer)}
893
+ theme={theme}
894
+ />
895
+ );
896
+
881
897
  return (
882
898
  <SlashSkillsContext.Provider value={skills}>
883
- <ThemeProvider theme={theme}>
884
- <AppShell
885
- sidebarOpen={sidebarOpen}
886
- onToggleSidebar={() => setSidebarOpen((open) => !open)}
887
- onOpenMenu={() => setMenuDialog("settings")}
888
- threadsPanel={
889
- <scrollbox style={{ flexGrow: 1 }} focused={false}>
890
- <ThreadTree
891
- rows={grouped.rows}
892
- cursor={inboxCursor}
893
- activeId={activeSession.id}
894
- pinnedIds={pinnedIds}
895
- width={30}
896
- onSelect={(id) => controller.open(id)}
897
- onPin={togglePin}
898
- onRename={(id, title) =>
899
- setMode({ type: "renameThread", sessionId: id, text: title })
900
- }
901
- theme={theme}
902
- />
903
- </scrollbox>
904
- }
905
- threadTitle={threadTitle(activeSession)}
906
- threadActions={
907
- showOwnerActions ? (
908
- <Toolbar>
909
- <Button onPress={onEditRequest} theme={theme}>
910
- {" edit "}
911
- </Button>
912
- <Button onPress={onShareRequest} theme={theme}>
913
- {" share "}
914
- </Button>
915
- </Toolbar>
916
- ) : undefined
917
- }
918
- threadPanel={
919
- <box style={{ flexGrow: 1, flexDirection: "column" }}>
920
- <box style={{ flexGrow: 1, flexDirection: "row" }}>
921
- {isPixelPrototype ? (
922
- <PrototypePixels
923
- prototypePath={prototypePath}
924
- canComment={prototypeCanComment}
925
- onCommentElement={onCommentPrototype}
926
- onComposingChange={setPrototypeComposing}
927
- hidden={chromeHidden}
928
- />
929
- ) : isDiff ? (
930
- <box style={{ flexGrow: 1, paddingLeft: 2, paddingTop: 1 }}>
931
- <text fg={theme.textDim}>review the changes on the right</text>
932
- </box>
933
- ) : (
934
- <ThreadView
935
- session={activeSession}
936
- suspended={threadViewSuspended}
937
- editOrphanCount={editOrphanCount}
938
- onComposingChange={setThreadComposing}
939
- resolved={resolved}
940
- onObserverBlocked={(reason) =>
941
- controller.setStatus(
942
- reason === "observer"
943
- ? "observer - read-only"
944
- : "review submitted - read-only",
945
- )
946
- }
947
- onCursorChange={setCursor}
948
- focusedAnnotationId={focusedAnnotationId}
949
- onFocusAnnotation={setFocusedAnnotationId}
950
- display={display}
951
- marks={marks}
952
- quickActions={quickActions}
953
- observer={observer}
954
- onAnnotate={(span, body) =>
955
- void controller.annotate(
956
- "comment",
957
- span.start.blockIndex,
958
- span.start.char,
959
- span.end.char,
899
+ <PaletteNamesContext.Provider value={paletteNames}>
900
+ <ThemeProvider theme={theme}>
901
+ <AppShell
902
+ sidebarOpen={sidebarOpen}
903
+ onToggleSidebar={() => setSidebarOpen((open) => !open)}
904
+ onOpenMenu={() => setMenuDialog("settings")}
905
+ threadsPanel={
906
+ <scrollbox style={{ flexGrow: 1 }} focused={false}>
907
+ <ThreadTree
908
+ rows={grouped.rows}
909
+ cursor={inboxCursor}
910
+ activeId={activeSession.id}
911
+ pinnedIds={pinnedIds}
912
+ width={30}
913
+ onSelect={(id) => controller.open(id)}
914
+ onPin={togglePin}
915
+ onRename={(id, title) =>
916
+ setMode({ type: "renameThread", sessionId: id, text: title })
917
+ }
918
+ theme={theme}
919
+ />
920
+ </scrollbox>
921
+ }
922
+ threadTitle={threadTitle(activeSession)}
923
+ threadActions={
924
+ showOwnerActions ? (
925
+ <Toolbar>
926
+ <Button onPress={onEditRequest} theme={theme}>
927
+ {" edit "}
928
+ </Button>
929
+ <Button onPress={onShareRequest} theme={theme}>
930
+ {" share "}
931
+ </Button>
932
+ </Toolbar>
933
+ ) : undefined
934
+ }
935
+ threadPanel={
936
+ <box style={{ flexGrow: 1, flexDirection: "column" }}>
937
+ <box style={{ flexGrow: 1, flexDirection: "row" }}>
938
+ {isPixelPrototype ? (
939
+ <PrototypePixels
940
+ prototypePath={prototypePath}
941
+ canComment={prototypeCanComment}
942
+ onCommentElement={onCommentPrototype}
943
+ onComposingChange={setPrototypeComposing}
944
+ hidden={chromeHidden}
945
+ />
946
+ ) : isDiff ? (
947
+ <box style={{ flexGrow: 1, paddingLeft: 2, paddingTop: 1 }}>
948
+ <text fg={theme.textDim}>review the changes on the right</text>
949
+ </box>
950
+ ) : (
951
+ <ThreadView
952
+ session={activeSession}
953
+ suspended={threadViewSuspended}
954
+ editOrphanCount={editOrphanCount}
955
+ onComposingChange={setThreadComposing}
956
+ resolved={resolved}
957
+ onObserverBlocked={(reason) =>
958
+ controller.setStatus(
959
+ reason === "observer"
960
+ ? "observer - read-only"
961
+ : "review submitted - read-only",
962
+ )
963
+ }
964
+ onCursorChange={setCursor}
965
+ focusedAnnotationId={focusedAnnotationId}
966
+ onFocusAnnotation={setFocusedAnnotationId}
967
+ display={display}
968
+ marks={marks}
969
+ quickActions={quickActions}
970
+ observer={observer}
971
+ onAnnotate={(span, body) =>
972
+ void controller.annotate(
973
+ "comment",
974
+ span.start.blockIndex,
975
+ span.start.char,
976
+ span.end.char,
977
+ body,
978
+ span.end.blockIndex,
979
+ )
980
+ }
981
+ onReply={(rootAnnotationId, body) =>
982
+ void controller.reply(rootAnnotationId, body)
983
+ }
984
+ onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
985
+ onExit={() => onExit?.(0)}
986
+ />
987
+ )}
988
+ </box>
989
+ {threadFooter}
990
+ </box>
991
+ }
992
+ changesOpen={workbench.changesOpen}
993
+ projectOpen={workbench.projectOpen}
994
+ onToggleChanges={workbench.toggleChanges}
995
+ onToggleProject={workbench.toggleProject}
996
+ onToggleRight={workbench.toggleRight}
997
+ projectMode={workbench.projectMode}
998
+ zoomHideThread={workbench.zoomed}
999
+ changesFooter={threadFooter}
1000
+ changesPanel={
1001
+ <EditorGrid
1002
+ tree={workbench.grid}
1003
+ focusedGroupId={workbench.activeGroup}
1004
+ commentCounts={diffCommentCounts}
1005
+ onFocusGroup={workbench.focusGroup}
1006
+ onActivateTab={workbench.activate}
1007
+ onCloseTab={workbench.close}
1008
+ onSplit={workbench.split}
1009
+ onZoom={workbench.toggleZoom}
1010
+ zoomed={workbench.zoomed}
1011
+ renderTab={(tab, groupFocused) => (
1012
+ <GridTabContent
1013
+ tab={tab}
1014
+ rows={rows}
1015
+ surface={{
1016
+ session: activeSession,
1017
+ quickActions,
1018
+ observer,
1019
+ commentsEnabled: true,
1020
+ resolved,
1021
+ suspended: threadViewSuspended || !groupFocused,
1022
+ onComposingChange: setThreadComposing,
1023
+ onObserverBlocked: (reason) =>
1024
+ controller.setStatus(
1025
+ reason === "observer"
1026
+ ? "observer - read-only"
1027
+ : "review submitted - read-only",
1028
+ ),
1029
+ onCursorChange: setCursor,
1030
+ focusedAnnotationId,
1031
+ onFocusAnnotation: setFocusedAnnotationId,
1032
+ // a diff thread's Changes view is the artifact itself; any other thread's is the
1033
+ // live working-tree diff, so its notes carry a file target and anchor there
1034
+ onAnnotate: (span, body) =>
1035
+ void controller.annotate(
1036
+ "comment",
1037
+ span.start.blockIndex,
1038
+ span.start.char,
1039
+ span.end.char,
1040
+ body,
1041
+ span.end.blockIndex,
1042
+ isDiff
1043
+ ? { kind: "artifact" }
1044
+ : { kind: "file", path: "", rev: "worktree" },
1045
+ ),
1046
+ onReply: (rootAnnotationId, body) =>
1047
+ void controller.reply(rootAnnotationId, body),
1048
+ onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
1049
+ onExit: () => onExit?.(0),
1050
+ }}
1051
+ rejectedRows={rejectedRows}
1052
+ fold={diffFold}
1053
+ fileStats={controller.fileStats()}
1054
+ split={diffView === "split" && workbench.zoomed}
1055
+ dimmed={walking}
1056
+ readFile={(path) => controller.repoReadFile(path)}
1057
+ onAddFileComment={(path, anchor, body) =>
1058
+ void controller.addComment(
1059
+ anchor,
1060
+ { kind: "file", path, rev: "worktree" },
960
1061
  body,
961
- span.end.blockIndex,
962
1062
  )
963
1063
  }
964
- onReply={(rootAnnotationId, body) =>
965
- void controller.reply(rootAnnotationId, body)
966
- }
967
- onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
968
- onExit={() => onExit?.(0)}
1064
+ theme={theme}
969
1065
  />
970
1066
  )}
971
- </box>
972
- <ThreadFooter
973
- repo={projectName(activeSession.workspace)}
974
- branch={activeSession.workspace.branch}
975
- onSubmit={onSubmitRequest}
976
- canSubmit={canSubmitReview(isOwner, resolved, observer)}
977
1067
  theme={theme}
978
1068
  />
979
- </box>
980
- }
981
- changesOpen={workbench.changesOpen}
982
- projectOpen={workbench.projectOpen}
983
- onToggleChanges={workbench.toggleChanges}
984
- onToggleProject={workbench.toggleProject}
985
- onToggleRight={workbench.toggleRight}
986
- projectMode={workbench.projectMode}
987
- zoomHideThread={workbench.zoomed}
988
- changesPanel={
989
- <EditorGrid
990
- tree={workbench.grid}
991
- focusedGroupId={workbench.activeGroup}
992
- commentCounts={diffCommentCounts}
993
- onFocusGroup={workbench.focusGroup}
994
- onActivateTab={workbench.activate}
995
- onCloseTab={workbench.close}
996
- onSplit={workbench.split}
997
- onZoom={workbench.toggleZoom}
998
- zoomed={workbench.zoomed}
999
- renderTab={(tab, groupFocused) => (
1000
- <GridTabContent
1001
- tab={tab}
1002
- rows={rows}
1003
- surface={{
1004
- session: activeSession,
1005
- quickActions,
1006
- observer,
1007
- commentsEnabled: true,
1008
- resolved,
1009
- suspended: threadViewSuspended || !groupFocused,
1010
- onComposingChange: setThreadComposing,
1011
- onObserverBlocked: (reason) =>
1012
- controller.setStatus(
1013
- reason === "observer"
1014
- ? "observer - read-only"
1015
- : "review submitted - read-only",
1016
- ),
1017
- onCursorChange: setCursor,
1018
- focusedAnnotationId,
1019
- onFocusAnnotation: setFocusedAnnotationId,
1020
- // a diff thread's Changes view is the artifact itself; any other thread's is the
1021
- // live working-tree diff, so its notes carry a file target and anchor there
1022
- onAnnotate: (span, body) =>
1023
- void controller.annotate(
1024
- "comment",
1025
- span.start.blockIndex,
1026
- span.start.char,
1027
- span.end.char,
1028
- body,
1029
- span.end.blockIndex,
1030
- isDiff ? { kind: "artifact" } : { kind: "file", path: "", rev: "worktree" },
1031
- ),
1032
- onReply: (rootAnnotationId, body) =>
1033
- void controller.reply(rootAnnotationId, body),
1034
- onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
1035
- onExit: () => onExit?.(0),
1036
- }}
1037
- rejectedRows={rejectedRows}
1038
- fold={diffFold}
1039
- fileStats={controller.fileStats()}
1040
- split={diffView === "split" && workbench.zoomed}
1041
- dimmed={walking}
1042
- readFile={(path) => controller.repoReadFile(path)}
1043
- onAddFileComment={(path, anchor, body) =>
1044
- void controller.addComment(
1045
- anchor,
1046
- { kind: "file", path, rev: "worktree" },
1047
- body,
1048
- )
1049
- }
1050
- theme={theme}
1051
- />
1052
- )}
1053
- theme={theme}
1054
- />
1055
- }
1056
- projectPanel={
1057
- <ProjectPanelBody
1058
- mode={workbench.projectMode}
1059
- loadChanges={() => controller.repoChanges()}
1060
- loadProjectFiles={() => controller.repoFiles()}
1061
- reloadKey={activeSession.id}
1062
- // the Changes navigator always opens a changed file as a diff - a diff review shows its
1063
- // captured snapshot, every other thread the live working-tree diff
1064
- onOpenChangedFile={(path) => workbench.openFile(path, "diff")}
1065
- onOpenProjectFile={(path) => workbench.openFile(path, "contents")}
1066
- commentCounts={diffCommentCounts}
1069
+ }
1070
+ projectPanel={
1071
+ <ProjectPanelBody
1072
+ mode={workbench.projectMode}
1073
+ loadChanges={() => controller.repoChanges()}
1074
+ loadProjectFiles={() => controller.repoFiles()}
1075
+ reloadKey={activeSession.id}
1076
+ // the Changes navigator always opens a changed file as a diff - a diff review shows its
1077
+ // captured snapshot, every other thread the live working-tree diff
1078
+ onOpenChangedFile={(path) => workbench.openFile(path, "diff")}
1079
+ onOpenProjectFile={(path) => workbench.openFile(path, "contents")}
1080
+ commentCounts={diffCommentCounts}
1081
+ theme={theme}
1082
+ />
1083
+ }
1084
+ theme={theme}
1085
+ >
1086
+ <TrailingOverlays
1087
+ walking={walking}
1088
+ walk={walk}
1089
+ walkFileList={walkFileList}
1090
+ viewedPaths={viewedPaths}
1091
+ session={activeSession}
1092
+ terminalWidth={terminalWidth}
1067
1093
  theme={theme}
1094
+ mode={mode}
1095
+ toast={toast}
1096
+ setMode={setMode}
1097
+ dispatch={dispatch}
1068
1098
  />
1069
- }
1070
- theme={theme}
1071
- >
1072
- <TrailingOverlays
1073
- walking={walking}
1074
- walk={walk}
1075
- walkFileList={walkFileList}
1076
- viewedPaths={viewedPaths}
1077
- session={activeSession}
1078
- terminalWidth={terminalWidth}
1079
- theme={theme}
1080
- mode={mode}
1081
- toast={toast}
1082
- setMode={setMode}
1083
- dispatch={dispatch}
1084
- />
1085
- {submitConfirmState !== null ? (
1086
- <box
1087
- style={{
1088
- position: "absolute",
1089
- left: 0,
1090
- top: 0,
1091
- width: "100%",
1092
- height: "100%",
1093
- justifyContent: "center",
1094
- alignItems: "center",
1095
- }}
1096
- >
1097
- <ConfirmCard {...submitConfirmState} theme={theme} />
1098
- </box>
1099
- ) : null}
1100
- {menuChrome}
1101
- </AppShell>
1102
- </ThemeProvider>
1099
+ {submitConfirmState !== null ? (
1100
+ <box
1101
+ style={{
1102
+ position: "absolute",
1103
+ left: 0,
1104
+ top: 0,
1105
+ width: "100%",
1106
+ height: "100%",
1107
+ justifyContent: "center",
1108
+ alignItems: "center",
1109
+ }}
1110
+ >
1111
+ <ConfirmCard {...submitConfirmState} theme={theme} />
1112
+ </box>
1113
+ ) : null}
1114
+ {menuChrome}
1115
+ </AppShell>
1116
+ </ThemeProvider>
1117
+ </PaletteNamesContext.Provider>
1103
1118
  </SlashSkillsContext.Provider>
1104
1119
  );
1105
1120
  }
@@ -247,6 +247,40 @@ describe("the four-pane workbench", () => {
247
247
  expect(diffToggleColumn(setup)).toBeGreaterThan(0);
248
248
  });
249
249
 
250
+ test("selecting a non-diff thread while zoomed exits zoom and restores the Thread pane", async () => {
251
+ // a second, non-diff thread to navigate to; its sync has no Changes editor to fill a zoom
252
+ server.core.sessionCreate({
253
+ workspace: { repoRoot: repo, branch: "main" },
254
+ artifact: {
255
+ type: "plan",
256
+ content: "# Zoomed Plan\n\nReview this plan.\n",
257
+ meta: { title: "Zoomed Plan" },
258
+ },
259
+ });
260
+ const setup = await renderApp();
261
+
262
+ // open the collapsed Threads sidebar first (its toggle sits three cells left of the brand); the
263
+ // brand rides the Thread header, which zoom then hides, so it must be reached before zooming
264
+ const brand = locateText(setup, "cueloop");
265
+ await setup.mockMouse.click(brand.column - 3, brand.row);
266
+ await waitForText(setup, "Zoomed Plan");
267
+
268
+ // zoom the diff - the Thread pane hides and the footer rides the Changes pane
269
+ const zoom = locateText(setup, NERD.zoom);
270
+ await setup.mockMouse.click(zoom.column, zoom.row);
271
+ await waitForState(setup, () => !setup.captureCharFrame().includes("review the changes"));
272
+
273
+ // switch to the plan thread: it has no Changes editor, so a stranded zoom would blank the middle
274
+ const planRow = locateText(setup, "Zoomed Plan");
275
+ await setup.mockMouse.click(planRow.column, planRow.row);
276
+ await waitForText(setup, "Review this plan.");
277
+
278
+ const frame = setup.captureCharFrame();
279
+ // the Thread pane is back with the plan, and the diff's Changes editor is gone - never a blank middle
280
+ expect(frame).toContain("Review this plan.");
281
+ expect(frame).not.toContain("store.ts");
282
+ });
283
+
250
284
  test("toggling the right sidebar while zoomed exits zoom and restores the Thread pane", async () => {
251
285
  const setup = await renderApp();
252
286
 
@@ -78,7 +78,6 @@ export const SlashPaletteOpen: Story = {
78
78
  slashActive
79
79
  slashItems={slashItemsFrom(DEFAULT_QUICK_ACTIONS)}
80
80
  slashIndex={1}
81
- inline={null}
82
81
  tokens={DARK}
83
82
  />
84
83
  ),