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

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.71",
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.71",
31
+ "@cueloop/extension-api": "0.1.0-alpha.71",
32
+ "@cueloop/integration-obsidian": "0.1.0-alpha.71",
33
+ "@cueloop/schema": "0.1.0-alpha.71",
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
@@ -878,6 +878,16 @@ export function App({
878
878
  dispatch({ type: "openSubmit" });
879
879
  };
880
880
 
881
+ const threadFooter = (
882
+ <ThreadFooter
883
+ repo={projectName(activeSession.workspace)}
884
+ branch={activeSession.workspace.branch}
885
+ onSubmit={onSubmitRequest}
886
+ canSubmit={canSubmitReview(isOwner, resolved, observer)}
887
+ theme={theme}
888
+ />
889
+ );
890
+
881
891
  return (
882
892
  <SlashSkillsContext.Provider value={skills}>
883
893
  <ThemeProvider theme={theme}>
@@ -969,13 +979,7 @@ export function App({
969
979
  />
970
980
  )}
971
981
  </box>
972
- <ThreadFooter
973
- repo={projectName(activeSession.workspace)}
974
- branch={activeSession.workspace.branch}
975
- onSubmit={onSubmitRequest}
976
- canSubmit={canSubmitReview(isOwner, resolved, observer)}
977
- theme={theme}
978
- />
982
+ {threadFooter}
979
983
  </box>
980
984
  }
981
985
  changesOpen={workbench.changesOpen}
@@ -985,6 +989,7 @@ export function App({
985
989
  onToggleRight={workbench.toggleRight}
986
990
  projectMode={workbench.projectMode}
987
991
  zoomHideThread={workbench.zoomed}
992
+ changesFooter={threadFooter}
988
993
  changesPanel={
989
994
  <EditorGrid
990
995
  tree={workbench.grid}
@@ -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
 
@@ -37,6 +37,8 @@ export interface AppShellProps {
37
37
  projectMode: ProjectPanelMode;
38
38
  /** The Changes editor grid; rendered only when changesOpen. */
39
39
  changesPanel?: React.ReactNode;
40
+ /** The thread footer, shown beneath the Changes grid while the Thread pane is zoomed away. */
41
+ changesFooter?: React.ReactNode;
40
42
  projectPanel: React.ReactNode;
41
43
  /** Hide the Thread pane so Changes fills the middle (zoom); the sidebars stay. */
42
44
  zoomHideThread?: boolean;
@@ -90,6 +92,7 @@ export function AppShell({
90
92
  onToggleRight,
91
93
  projectMode,
92
94
  changesPanel,
95
+ changesFooter,
93
96
  projectPanel,
94
97
  zoomHideThread,
95
98
  footer,
@@ -227,7 +230,8 @@ export function AppShell({
227
230
  borderColor: tokens.border,
228
231
  }}
229
232
  >
230
- {changesPanel}
233
+ <box style={{ flexGrow: 1, minHeight: 0 }}>{changesPanel}</box>
234
+ {zoomHideThread ? changesFooter : null}
231
235
  </box>
232
236
  ) : null}
233
237
  {projectOpen ? (
@@ -80,6 +80,47 @@ describe("zoom icon in the shell", () => {
80
80
  }
81
81
  }
82
82
 
83
+ test("the footer rides the Changes pane only while the Thread pane is zoomed away", async () => {
84
+ async function footerVisible(zoomed: boolean): Promise<boolean> {
85
+ const setup = await testRender(
86
+ <AppShell
87
+ sidebarOpen={false}
88
+ onToggleSidebar={() => {}}
89
+ onOpenMenu={() => {}}
90
+ threadsPanel={<text>threads</text>}
91
+ threadTitle="thread"
92
+ threadPanel={<text>body</text>}
93
+ changesOpen
94
+ projectOpen
95
+ onToggleChanges={() => {}}
96
+ onToggleProject={() => {}}
97
+ onToggleRight={() => {}}
98
+ projectMode="changes"
99
+ zoomHideThread={zoomed}
100
+ projectPanel={<text>project</text>}
101
+ changesFooter={<text>send message</text>}
102
+ changesPanel={<text>diff</text>}
103
+ />,
104
+ { width: 160, height: 8 },
105
+ );
106
+
107
+ allowEventLoopUpdates();
108
+ await setup.waitForVisualIdle();
109
+ const shown = setup.captureCharFrame().includes("send message");
110
+
111
+ setup.renderer.destroy();
112
+
113
+ return shown;
114
+ }
115
+
116
+ // Act
117
+ const [zoomed, unzoomed] = await Promise.all([footerVisible(true), footerVisible(false)]);
118
+
119
+ // Assert - the Thread pane carries the footer normally, so it must not double up in Changes
120
+ expect(zoomed).toBe(true);
121
+ expect(unzoomed).toBe(false);
122
+ });
123
+
83
124
  test("the icon clears the Project border by its padding in both states", async () => {
84
125
  // Act
85
126
  const setupColumns = await Promise.all([
@@ -1,6 +1,6 @@
1
- // The thread's bottom band: a header-like bar shown only under the always-visible
2
- // thread column. It carries the repo/branch context on the left and a written,
3
- // clickable "Send message" control on the right.
1
+ // The thread's bottom band: a header-like bar under the thread column, or the
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.
4
4
 
5
5
  import React from "react";
6
6
  import { DARK, type Theme } from "../theme";
@@ -64,6 +64,8 @@ export function useChangesWorkbench(options?: ChangesWorkbenchOptions): ChangesW
64
64
  setChangesOpen(isDiff);
65
65
  setProjectMode(isDiff ? "changes" : "tree");
66
66
  rememberedChanges.current = isDiff;
67
+ // a non-diff thread has no Changes editor; leaving zoom on would strand the hidden Thread pane
68
+ if (!isDiff) setZoomed(false);
67
69
  setGrid(makeGroup([changesTab()]));
68
70
  setFocusedGroup(null);
69
71
  };