@meistrari/remy-cli 1.1.1 → 1.3.0

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.
Files changed (3) hide show
  1. package/README.md +7 -3
  2. package/dist/remy.js +337 -111
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -42,19 +42,23 @@ Use `Tab` while writing the request to complete a local file path. Remy attaches
42
42
 
43
43
  ### Resume or follow up
44
44
 
45
- Use the arrow keys to select a session in the dashboard and press `Enter` to open it. In a session, type a follow-up and press `Enter` to send it.
45
+ Use Up/Down to select a session in the dashboard, Left/Right to load the adjacent page, and `Enter` to open it. After the dashboard list or repository-review list has focus, Vim keys work too: `j`/`k` move, `h`/`l` go to the previous/next dashboard page (or clear/mark a repository), `G` goes to the last row, and `gg` goes to the first row. Press `Shift+L` in the dashboard to start a logout confirmation. In a session, type a follow-up and press `Enter` to send it.
46
+
47
+ Drag to select visible text in any Remy view; Remy copies it to the local clipboard and emits OSC 52 for terminal or remote-session clipboard support, then clears the selection highlight. When the conversation has focus, press `Tab` to return to the composer.
46
48
 
47
49
  | Control | Result |
48
50
  | --- | --- |
49
51
  | `Esc` | Interrupt the active turn; the session stays open. |
50
52
  | `/complete` | Complete an open session after its active turn stops. |
53
+ | `/cancel` | Cancel the current session, including an active turn. |
51
54
  | `/sessions` | Return to the dashboard. |
52
55
  | `/new` | Start another new-session flow. |
56
+ | `/logout` | Sign out of Remy after confirmation. |
53
57
  | `/help` | Show session controls. |
54
58
  | `/exit` | Leave Remy. |
55
59
  | `Ctrl+O` | Expand or collapse activity details. |
56
60
 
57
- After a session becomes terminal, press `Esc` to return to the dashboard or `n` to start new work. From the dashboard, press `l` to log out or `q` to quit. `Ctrl+C` exits Remy from the dashboard, wizard, or session view.
61
+ After a session becomes terminal, press `Esc` to return to the dashboard or `n` to start new work. From the dashboard, press `q` to quit. `Shift+L` in the dashboard and `/logout` in a session ask for confirmation; press `y` to sign out or `Esc` to cancel. `Ctrl+C` exits Remy from the dashboard, wizard, or session view.
58
62
 
59
63
  ## Commands for direct or scripted use
60
64
 
@@ -112,7 +116,7 @@ remy login \
112
116
  --target-application-id <target-application-uuid>
113
117
  ```
114
118
 
115
- Remy stores endpoint configuration in `~/.config/remy/config.json` and tokens in `~/.local/state/remy/tokens.json`. Set `XDG_CONFIG_HOME` or `XDG_STATE_HOME` to use different base directories. `remy logout` removes tokens but retains endpoint configuration; pass `--api-url` to require that the supplied URL matches the active API host before logging out.
119
+ Remy stores endpoint configuration in `~/.config/remy/config.json` and tokens in `~/.local/state/remy/tokens.json`. Set `XDG_CONFIG_HOME` or `XDG_STATE_HOME` to use different base directories. It refreshes an expired access token before each Coding Agent API request, so long-running sessions and reconnects remain authenticated. `remy logout` removes tokens but retains endpoint configuration; pass `--api-url` to require that the supplied URL matches the active API host before logging out.
116
120
 
117
121
  ## Complete command reference
118
122
 
package/dist/remy.js CHANGED
@@ -32820,6 +32820,13 @@ async function completeSession({ client, sessionId }) {
32820
32820
  throw new CodingAgentProtocolError("Session completion response did not match the public API contract.", { cause: parsed.error });
32821
32821
  return parsed.data;
32822
32822
  }
32823
+ async function cancelSession({ client, sessionId }) {
32824
+ const response = await client.request(`/v1/sessions/${encodeURIComponent(sessionId)}/cancel`, { method: "PUT" });
32825
+ const parsed = sessionLifecycleResponseSchema.safeParse(await parseJson2(response, "Session cancellation response was not valid JSON."));
32826
+ if (!parsed.success)
32827
+ throw new CodingAgentProtocolError("Session cancellation response did not match the public API contract.", { cause: parsed.error });
32828
+ return parsed.data;
32829
+ }
32823
32830
  async function listSessions({
32824
32831
  client,
32825
32832
  limit,
@@ -34355,7 +34362,7 @@ async function sleepMs(ms, signal) {
34355
34362
  }
34356
34363
 
34357
34364
  // src/tui/dashboard.ts
34358
- import { bg, BoxRenderable, bold as bold2, CliRenderEvents, fg as fg3, InputRenderable, ScrollBoxRenderable, stringToStyledText as stringToStyledText2, TextRenderable } from "@opentui/core";
34365
+ import { bg, BoxRenderable, bold as bold2, CliRenderEvents as CliRenderEvents2, fg as fg3, ScrollBoxRenderable, stringToStyledText as stringToStyledText2, TextRenderable } from "@opentui/core";
34359
34366
 
34360
34367
  // src/tui/composer-divider.ts
34361
34368
  import { dim as dim2, fg as fg2, StyledText as StyledText2 } from "@opentui/core";
@@ -34551,18 +34558,20 @@ function renderComposerDivider({ width, tag, leadLabel }) {
34551
34558
  }
34552
34559
 
34553
34560
  // src/tui/renderer.ts
34554
- import { createCliRenderer } from "@opentui/core";
34561
+ import { CliRenderEvents, createClipboard, createCliRenderer, createHostClipboard, createRendererClipboardAdapter } from "@opentui/core";
34555
34562
  var rendererDestroyed = new WeakMap;
34556
34563
  var enterAlternateScreen = "\x1B[?1049h";
34557
34564
  var clearScreen = "\x1B[2J\x1B[H";
34558
34565
  var exitAlternateScreen = "\x1B[?1049l";
34559
34566
  var transitionScreenWriter;
34567
+ var ignoreTerminalFocus = (sequence) => sequence === "\x1B[I" || sequence === "\x1B[O";
34560
34568
  async function createRemyRenderer({
34561
34569
  createRenderer = createCliRenderer
34562
34570
  } = {}) {
34563
34571
  let resolveDestroyed = () => {
34564
34572
  return;
34565
34573
  };
34574
+ let selectionClipboard;
34566
34575
  const destroyed = new Promise((resolve) => {
34567
34576
  resolveDestroyed = resolve;
34568
34577
  });
@@ -34571,9 +34580,34 @@ async function createRemyRenderer({
34571
34580
  exitSignals: [],
34572
34581
  screenMode: "alternate-screen",
34573
34582
  useMouse: true,
34574
- onDestroy: resolveDestroyed
34583
+ prependInputHandlers: [ignoreTerminalFocus],
34584
+ onDestroy: () => {
34585
+ selectionClipboard?.dispose().catch(() => {
34586
+ return;
34587
+ });
34588
+ resolveDestroyed();
34589
+ }
34575
34590
  });
34576
34591
  transitionScreenWriter = undefined;
34592
+ try {
34593
+ selectionClipboard = createClipboard({
34594
+ host: createHostClipboard(),
34595
+ terminal: createRendererClipboardAdapter(renderer)
34596
+ });
34597
+ } catch {}
34598
+ renderer.on(CliRenderEvents.SELECTION, (selection) => {
34599
+ const text = selection.getSelectedText();
34600
+ if (text.length === 0)
34601
+ return;
34602
+ renderer.clearSelection();
34603
+ if (selectionClipboard) {
34604
+ selectionClipboard.writeText(text, { destination: "all-available" }).catch(() => {
34605
+ return;
34606
+ });
34607
+ return;
34608
+ }
34609
+ renderer.copyToClipboardOSC52(text);
34610
+ });
34577
34611
  rendererDestroyed.set(renderer, destroyed);
34578
34612
  return renderer;
34579
34613
  }
@@ -34611,6 +34645,7 @@ async function createDashboardTui({
34611
34645
  }) {
34612
34646
  const renderer = await createRenderer();
34613
34647
  let inputHandler;
34648
+ let keyHandler;
34614
34649
  let resizeHandler;
34615
34650
  let rendererDestroyed2 = false;
34616
34651
  try {
@@ -34637,8 +34672,7 @@ async function createDashboardTui({
34637
34672
  previewBand.visible = showPreview;
34638
34673
  previewBand.content = showPreview ? formatSessionPreview({ session: selectedSession, width: contentWidth }) : stringToStyledText2("");
34639
34674
  separator.content = renderComposerDivider({ width: contentWidth });
34640
- footer.content = logoutConfirmationOpen ? "Log out of Remy? y confirms \xB7 Esc cancels" : dashboardFooter({ loadingPage, pageError, page, rowsBelow });
34641
- input.placeholder = logoutConfirmationOpen ? "Log out of Remy?" : "Type here to start a new session";
34675
+ footer.content = dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen });
34642
34676
  renderer.requestRender();
34643
34677
  }, finish = function(value) {
34644
34678
  if (settled)
@@ -34650,6 +34684,11 @@ async function createDashboardTui({
34650
34684
  return;
34651
34685
  settled = true;
34652
34686
  rejectDraft(error93);
34687
+ }, moveSelectionTo = function(index) {
34688
+ if (page.sessions.length === 0)
34689
+ return;
34690
+ selectedIndex = Math.max(0, Math.min(index, page.sessions.length - 1));
34691
+ render();
34653
34692
  }, handleSessionKey = function(key) {
34654
34693
  if (logoutConfirmationOpen) {
34655
34694
  key.preventDefault();
@@ -34661,16 +34700,57 @@ async function createDashboardTui({
34661
34700
  }
34662
34701
  return;
34663
34702
  }
34703
+ const listFocused = renderer.currentFocusedRenderable === listRegion;
34664
34704
  if (key.name === "down") {
34665
34705
  key.preventDefault();
34706
+ awaitingVimGoToTop = false;
34666
34707
  moveSelection("next");
34667
34708
  return;
34668
34709
  }
34669
34710
  if (key.name === "up") {
34670
34711
  key.preventDefault();
34712
+ awaitingVimGoToTop = false;
34671
34713
  moveSelection("previous");
34672
34714
  return;
34673
34715
  }
34716
+ if (key.name === "right") {
34717
+ key.preventDefault();
34718
+ awaitingVimGoToTop = false;
34719
+ loadAdjacentPage("next");
34720
+ return;
34721
+ }
34722
+ if (key.name === "left") {
34723
+ key.preventDefault();
34724
+ awaitingVimGoToTop = false;
34725
+ loadAdjacentPage("previous");
34726
+ return;
34727
+ }
34728
+ if (listFocused && (key.name === "G" || key.name === "g" && key.shift)) {
34729
+ key.preventDefault();
34730
+ awaitingVimGoToTop = false;
34731
+ moveSelectionTo(page.sessions.length - 1);
34732
+ return;
34733
+ }
34734
+ if (listFocused && key.name === "g") {
34735
+ key.preventDefault();
34736
+ if (awaitingVimGoToTop)
34737
+ moveSelectionTo(0);
34738
+ awaitingVimGoToTop = !awaitingVimGoToTop;
34739
+ return;
34740
+ }
34741
+ if (listFocused && (key.name === "j" || key.name === "k")) {
34742
+ key.preventDefault();
34743
+ awaitingVimGoToTop = false;
34744
+ moveSelection(key.name === "j" ? "next" : "previous");
34745
+ return;
34746
+ }
34747
+ if (listFocused && !key.shift && (key.name === "h" || key.name === "l")) {
34748
+ key.preventDefault();
34749
+ awaitingVimGoToTop = false;
34750
+ loadAdjacentPage(key.name === "l" ? "next" : "previous");
34751
+ return;
34752
+ }
34753
+ awaitingVimGoToTop = false;
34674
34754
  if (key.name === "return" || key.name === "enter") {
34675
34755
  key.preventDefault();
34676
34756
  const session = page.sessions[selectedIndex];
@@ -34683,7 +34763,7 @@ async function createDashboardTui({
34683
34763
  finish({ kind: "new" });
34684
34764
  return;
34685
34765
  }
34686
- if (key.name === "l") {
34766
+ if (key.name === "L" || key.shift && key.name === "l") {
34687
34767
  key.preventDefault();
34688
34768
  logoutConfirmationOpen = true;
34689
34769
  render();
@@ -34706,20 +34786,23 @@ async function createDashboardTui({
34706
34786
  const previewBand = new TextRenderable(renderer, { content: "", flexShrink: 0 });
34707
34787
  const separator = new TextRenderable(renderer, { content: "", flexShrink: 0 });
34708
34788
  const footer = new TextRenderable(renderer, { content: "", flexShrink: 0 });
34709
- const input = new InputRenderable(renderer, { placeholder: "Describe the work to do" });
34789
+ root.onMouseDown = (event) => {
34790
+ event.preventDefault();
34791
+ listRegion.focus();
34792
+ };
34710
34793
  listRegion.add(content);
34711
34794
  root.add(statusBand);
34712
34795
  root.add(listRegion);
34713
34796
  root.add(previewBand);
34714
34797
  root.add(separator);
34715
34798
  root.add(footer);
34716
- root.add(input);
34717
34799
  renderer.root.add(root);
34718
34800
  let page = initialPage;
34719
34801
  let pageIndex = 1;
34720
34802
  let selectedIndex = 0;
34721
34803
  let loadingPage = false;
34722
34804
  let pageError;
34805
+ let awaitingVimGoToTop = false;
34723
34806
  let logoutConfirmationOpen = false;
34724
34807
  let destroyed = false;
34725
34808
  let rendererDestroyPromise;
@@ -34734,16 +34817,10 @@ async function createDashboardTui({
34734
34817
  resolveAction = resolve;
34735
34818
  rejectDraft = reject;
34736
34819
  });
34737
- async function moveSelection(direction) {
34820
+ async function loadAdjacentPage(direction) {
34738
34821
  if (loadingPage)
34739
34822
  return;
34740
- const isBoundary = direction === "next" ? selectedIndex === page.sessions.length - 1 : selectedIndex === 0;
34741
34823
  const canLoad = direction === "next" ? page.hasMore : page.canGoPrevious;
34742
- if (!isBoundary) {
34743
- selectedIndex += direction === "next" ? 1 : -1;
34744
- render();
34745
- return;
34746
- }
34747
34824
  if (!canLoad)
34748
34825
  return;
34749
34826
  loadingPage = true;
@@ -34760,6 +34837,17 @@ async function createDashboardTui({
34760
34837
  render();
34761
34838
  }
34762
34839
  }
34840
+ async function moveSelection(direction) {
34841
+ if (loadingPage || page.sessions.length === 0)
34842
+ return;
34843
+ const isBoundary = direction === "next" ? selectedIndex === page.sessions.length - 1 : selectedIndex === 0;
34844
+ if (!isBoundary) {
34845
+ selectedIndex += direction === "next" ? 1 : -1;
34846
+ render();
34847
+ return;
34848
+ }
34849
+ await loadAdjacentPage(direction);
34850
+ }
34763
34851
  inputHandler = (sequence) => {
34764
34852
  if (sequence === "\x03") {
34765
34853
  finish({ kind: "interrupt" });
@@ -34767,16 +34855,17 @@ async function createDashboardTui({
34767
34855
  }
34768
34856
  return false;
34769
34857
  };
34770
- input.onKeyDown = handleSessionKey;
34858
+ keyHandler = handleSessionKey;
34771
34859
  renderer.addInputHandler(inputHandler);
34772
- renderer.on(CliRenderEvents.RENDER_ERROR, (event) => fail(event.error));
34860
+ renderer.keyInput.on("keypress", keyHandler);
34861
+ renderer.on(CliRenderEvents2.RENDER_ERROR, (event) => fail(event.error));
34773
34862
  resizeHandler = () => {
34774
34863
  if (!destroyed)
34775
34864
  render();
34776
34865
  };
34777
- renderer.on(CliRenderEvents.RESIZE, resizeHandler);
34778
- input.focus();
34866
+ renderer.on(CliRenderEvents2.RESIZE, resizeHandler);
34779
34867
  render();
34868
+ listRegion.focus();
34780
34869
  return {
34781
34870
  waitForAction: async () => await action,
34782
34871
  destroy() {
@@ -34785,8 +34874,10 @@ async function createDashboardTui({
34785
34874
  destroyed = true;
34786
34875
  if (inputHandler)
34787
34876
  renderer.removeInputHandler(inputHandler);
34877
+ if (keyHandler)
34878
+ renderer.keyInput.off("keypress", keyHandler);
34788
34879
  if (resizeHandler)
34789
- renderer.off(CliRenderEvents.RESIZE, resizeHandler);
34880
+ renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
34790
34881
  rendererDestroyed2 = true;
34791
34882
  renderer.destroy();
34792
34883
  },
@@ -34797,8 +34888,10 @@ async function createDashboardTui({
34797
34888
  } catch (error93) {
34798
34889
  if (inputHandler)
34799
34890
  renderer.removeInputHandler(inputHandler);
34891
+ if (keyHandler)
34892
+ renderer.keyInput.off("keypress", keyHandler);
34800
34893
  if (resizeHandler)
34801
- renderer.off(CliRenderEvents.RESIZE, resizeHandler);
34894
+ renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
34802
34895
  if (!rendererDestroyed2) {
34803
34896
  rendererDestroyed2 = true;
34804
34897
  renderer.destroy();
@@ -34811,22 +34904,24 @@ function formatSessionRow({ session, selected, width }) {
34811
34904
  const outcome = sessionOutcome(session);
34812
34905
  const updated = formatUpdatedAt(session.updatedAt ?? session.createdAt);
34813
34906
  const prefix = `#${session.sessionNumber} `;
34814
- const title = truncate(session.title ?? "Untitled session", Math.max(8, width - prefix.length - activity.label.length - outcome.length - updated.length - 8));
34907
+ const title = truncate(session.title ?? "Untitled session", Math.max(8, Math.min(40, Math.floor(width * 0.24))));
34908
+ const attribution = `${session.creator} (${session.source})`;
34909
+ const metadataWidth = Math.max(0, width - prefix.length - title.length - activity.label.length - 6);
34910
+ const metadata = truncate(`${attribution} \xB7 ${outcome} \xB7 ${updated}`, metadataWidth);
34815
34911
  const line = [
34816
34912
  fg3(PALETTE.dimText)(prefix),
34817
34913
  fg3(PALETTE.bodyText)(title),
34818
34914
  fg3(PALETTE.dimText)(" "),
34819
34915
  bold2(fg3(activity.color)(activity.label)),
34820
34916
  fg3(PALETTE.dimText)(" "),
34821
- fg3(PALETTE.bodyText)(outcome),
34822
- fg3(PALETTE.dimText)(` ${updated}`)
34917
+ fg3(PALETTE.dimText)(metadata)
34823
34918
  ];
34824
34919
  const lineLength = line.reduce((total, chunk) => total + chunk.text.length, 0);
34825
34920
  const padded = selected ? [...line, fg3(PALETTE.dimText)(" ".repeat(Math.max(0, width - lineLength)))] : line;
34826
34921
  return selected ? padded.map((chunk) => bg(PALETTE.selectionBg)(chunk)) : padded;
34827
34922
  }
34828
34923
  function visibleListWindow({ sessionCount, selectedIndex, height }) {
34829
- const rowCount = Math.max(1, height - 10);
34924
+ const rowCount = Math.max(1, height - 9);
34830
34925
  const firstIndex = Math.min(Math.max(0, selectedIndex - Math.floor(rowCount / 2)), Math.max(0, sessionCount - rowCount));
34831
34926
  return { firstIndex, rowCount };
34832
34927
  }
@@ -34929,7 +35024,9 @@ function dashboardStatusBand({ page, pageIndex }) {
34929
35024
  paging.push("more \u2192");
34930
35025
  return `remy \xB7 sessions \xB7 ${openCount} open \xB7 ${paging.join(" \xB7 ")}`;
34931
35026
  }
34932
- function dashboardFooter({ loadingPage, pageError, page, rowsBelow }) {
35027
+ function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen }) {
35028
+ if (logoutConfirmationOpen)
35029
+ return "Log out of Remy? y confirms \xB7 Esc cancels";
34933
35030
  const parts = [];
34934
35031
  if (loadingPage)
34935
35032
  parts.push("Loading sessions\u2026");
@@ -34937,9 +35034,7 @@ function dashboardFooter({ loadingPage, pageError, page, rowsBelow }) {
34937
35034
  parts.push(`Could not load sessions: ${pageError}`);
34938
35035
  if (rowsBelow > 0)
34939
35036
  parts.push(`\u2193 ${rowsBelow} more below`);
34940
- if (page.hasMore || page.canGoPrevious)
34941
- parts.push("\u2191\u2193 pages at the ends of the list");
34942
- parts.push("\u2191\u2193 move \xB7 \u23CE open \xB7 n new session \xB7 q quit \xB7 ctrl+c discard & quit");
35037
+ parts.push("\u2191\u2193 move \xB7 \u23CE open \xB7 n new session \xB7 \u21E7l log out \xB7 q quit");
34943
35038
  return parts.join(" \xB7 ");
34944
35039
  }
34945
35040
  function truncate(value, width) {
@@ -34952,7 +35047,7 @@ async function createDefaultRenderer() {
34952
35047
  }
34953
35048
 
34954
35049
  // src/tui/new-session-wizard.ts
34955
- import { bg as bg3, BoxRenderable as BoxRenderable3, bold as bold3, CliRenderEvents as CliRenderEvents2, dim as dim3, fg as fg5, ScrollBoxRenderable as ScrollBoxRenderable2, StyledText as StyledText4, stringToStyledText as stringToStyledText4, TextRenderable as TextRenderable3 } from "@opentui/core";
35050
+ import { bg as bg3, BoxRenderable as BoxRenderable3, bold as bold3, CliRenderEvents as CliRenderEvents3, dim as dim3, fg as fg5, ScrollBoxRenderable as ScrollBoxRenderable2, StyledText as StyledText4, stringToStyledText as stringToStyledText4, TextRenderable as TextRenderable3 } from "@opentui/core";
34956
35051
 
34957
35052
  // src/tui/composer.ts
34958
35053
  import { BoxRenderable as BoxRenderable2, TextRenderable as TextRenderable2, TextareaRenderable } from "@opentui/core";
@@ -35297,7 +35392,7 @@ async function createNewSessionWizard({
35297
35392
  if (composerMounted)
35298
35393
  composer.render();
35299
35394
  if (repositoryListVisible) {
35300
- renderer.once(CliRenderEvents2.FRAME, () => {
35395
+ renderer.once(CliRenderEvents3.FRAME, () => {
35301
35396
  if (destroyed || step !== "repositories" && step !== "repositorySearch")
35302
35397
  return;
35303
35398
  const nextRepositoryHeaderHeight = Math.min(content.virtualLineCount, Math.floor(renderer.height * 0.45));
@@ -35526,6 +35621,24 @@ async function createNewSessionWizard({
35526
35621
  if (step !== "repositories")
35527
35622
  return;
35528
35623
  const choices = visibleRepositories();
35624
+ const repositoryListFocused = renderer.currentFocusedRenderable === repositoryScrollBox;
35625
+ if (repositoryListFocused && (key.name === "G" || key.name === "g" && key.shift)) {
35626
+ key.preventDefault();
35627
+ awaitingVimGoToTop = false;
35628
+ if (choices.length > 0)
35629
+ repositoryIndex = choices.length - 1;
35630
+ render();
35631
+ return;
35632
+ }
35633
+ if (repositoryListFocused && key.name === "g") {
35634
+ key.preventDefault();
35635
+ if (awaitingVimGoToTop && choices.length > 0)
35636
+ repositoryIndex = 0;
35637
+ awaitingVimGoToTop = !awaitingVimGoToTop;
35638
+ render();
35639
+ return;
35640
+ }
35641
+ awaitingVimGoToTop = false;
35529
35642
  if (key.name === "up" || key.name === "down" || key.name === "j" || key.name === "k") {
35530
35643
  key.preventDefault();
35531
35644
  if (choices.length > 0)
@@ -35599,6 +35712,7 @@ async function createNewSessionWizard({
35599
35712
  let completionIndex = 0;
35600
35713
  let pathCompletionMenuOpen = false;
35601
35714
  let repositoryHeaderHeight;
35715
+ let awaitingVimGoToTop = false;
35602
35716
  const manualSelections = new Map;
35603
35717
  let searchSelections;
35604
35718
  let suggestedIds = new Set;
@@ -35822,7 +35936,7 @@ async function createNewSessionWizard({
35822
35936
  };
35823
35937
  renderer.addInputHandler(inputHandler);
35824
35938
  renderer.keyInput.on("keypress", keyHandler);
35825
- renderer.on(CliRenderEvents2.RENDER_ERROR, (event) => fail(event.error));
35939
+ renderer.on(CliRenderEvents3.RENDER_ERROR, (event) => fail(event.error));
35826
35940
  resizeHandler = () => {
35827
35941
  if (destroyed)
35828
35942
  return;
@@ -35831,7 +35945,7 @@ async function createNewSessionWizard({
35831
35945
  repositoryIndex = Math.min(repositoryIndex, choices.length - 1);
35832
35946
  render();
35833
35947
  };
35834
- renderer.on(CliRenderEvents2.RESIZE, resizeHandler);
35948
+ renderer.on(CliRenderEvents3.RESIZE, resizeHandler);
35835
35949
  render();
35836
35950
  if (!Array.isArray(repositories))
35837
35951
  requestRepositoryLoad({ retry: false });
@@ -35846,7 +35960,7 @@ async function createNewSessionWizard({
35846
35960
  if (keyHandler)
35847
35961
  renderer.keyInput.off("keypress", keyHandler);
35848
35962
  if (resizeHandler)
35849
- renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
35963
+ renderer.off(CliRenderEvents3.RESIZE, resizeHandler);
35850
35964
  if (suggestionSpinnerTimer) {
35851
35965
  clearInterval(suggestionSpinnerTimer);
35852
35966
  suggestionSpinnerTimer = undefined;
@@ -35867,7 +35981,7 @@ async function createNewSessionWizard({
35867
35981
  if (keyHandler)
35868
35982
  renderer.keyInput.off("keypress", keyHandler);
35869
35983
  if (resizeHandler)
35870
- renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
35984
+ renderer.off(CliRenderEvents3.RESIZE, resizeHandler);
35871
35985
  if (!rendererDestroyed2) {
35872
35986
  rendererDestroyed2 = true;
35873
35987
  renderer.destroy();
@@ -35950,11 +36064,13 @@ async function createDefaultRenderer2() {
35950
36064
  }
35951
36065
 
35952
36066
  // src/tui/session-view.ts
35953
- import { CliRenderEvents as CliRenderEvents3, BoxRenderable as BoxRenderable4, bg as bg4, bold as bold4, dim as dim4, fg as fg6, ScrollBoxRenderable as ScrollBoxRenderable3, StyledText as StyledText5, stringToStyledText as stringToStyledText5, TextRenderable as TextRenderable4 } from "@opentui/core";
36067
+ import { CliRenderEvents as CliRenderEvents4, BoxRenderable as BoxRenderable4, bg as bg4, bold as bold4, dim as dim4, fg as fg6, ScrollBoxRenderable as ScrollBoxRenderable3, StyledText as StyledText5, stringToStyledText as stringToStyledText5, TextRenderable as TextRenderable4 } from "@opentui/core";
35954
36068
  var composerSlashCommands = [
35955
36069
  { value: "/sessions", description: "Back to the session list" },
35956
36070
  { value: "/complete", description: "Finish this session" },
36071
+ { value: "/cancel", description: "Cancel this session" },
35957
36072
  { value: "/new", description: "Start a new session" },
36073
+ { value: "/logout", description: "Sign out of Remy" },
35958
36074
  { value: "/help", description: "Show what you can do here" },
35959
36075
  { value: "/exit", description: "Leave Remy" }
35960
36076
  ];
@@ -35965,6 +36081,7 @@ async function createSessionTui({
35965
36081
  submitMessage,
35966
36082
  requestInterrupt,
35967
36083
  requestComplete,
36084
+ requestCancel,
35968
36085
  repositoryLabel,
35969
36086
  readClipboardImage,
35970
36087
  savePastedImage,
@@ -36045,8 +36162,8 @@ async function createSessionTui({
36045
36162
  const working = isRemyWorking(latestState);
36046
36163
  const startedAt = workingTurnStartedAt(latestState);
36047
36164
  const elapsedMs = startedAt === undefined ? 0 : Math.max(0, now3() - Date.parse(startedAt));
36048
- statusBand.content = renderStatusBand({ state: latestState, working, elapsedMs, stopState, completeState, repositoryLabel });
36049
- const liveIndicator = completeState.kind === "completing" ? renderWorkingIndicator({ frame: workingSpinnerFrames[spinnerFrameIndex], elapsedMs: 0, mode: "completing" }) : working ? renderWorkingIndicator({
36165
+ statusBand.content = renderStatusBand({ state: latestState, working, elapsedMs, stopState, lifecycleRequestState, repositoryLabel });
36166
+ const liveIndicator = lifecycleRequestState.kind === "pending" ? renderWorkingIndicator({ frame: workingSpinnerFrames[spinnerFrameIndex], elapsedMs: 0, mode: lifecycleRequestState.operation }) : working ? renderWorkingIndicator({
36050
36167
  frame: workingSpinnerFrames[spinnerFrameIndex],
36051
36168
  elapsedMs,
36052
36169
  mode: stopState.kind === "stopping" ? "stopping" : "working"
@@ -36057,8 +36174,8 @@ async function createSessionTui({
36057
36174
  `), joinStyled(liveContent, `
36058
36175
 
36059
36176
  `)], "");
36060
- if (completeState.kind === "completing")
36061
- composer.placeholder = "Completing the session \u2014 messages are paused.";
36177
+ if (lifecycleRequestState.kind === "pending")
36178
+ composer.placeholder = `${lifecycleOperationPresentParticiple(lifecycleRequestState.operation)} the session \u2014 messages are paused.`;
36062
36179
  if (latestState.aggregateStatus === "open")
36063
36180
  composerLayout.render();
36064
36181
  const submissionStatus = renderComposerStatus({ admittedSubmissions });
@@ -36075,7 +36192,7 @@ async function createSessionTui({
36075
36192
 
36076
36193
  `) : "";
36077
36194
  hints.visible = renderer.height >= 10;
36078
- hints.content = hints.visible ? renderActionBar({ state: latestState, working, stopState, completeState }) : "";
36195
+ hints.content = hints.visible ? renderActionBar({ state: latestState, working, stopState, lifecycleRequestState }) : "";
36079
36196
  renderer.requestRender();
36080
36197
  }, syncTerminalSessionControls = function() {
36081
36198
  const terminal = latestState.aggregateStatus !== "open";
@@ -36107,7 +36224,7 @@ async function createSessionTui({
36107
36224
  renderedAdmittedSubmissions = admittedSubmissions;
36108
36225
  renderedWorking = working;
36109
36226
  }, spinnerActive = function() {
36110
- return isRemyWorking(latestState) || completeState.kind === "completing";
36227
+ return isRemyWorking(latestState) || lifecycleRequestState.kind === "pending";
36111
36228
  }, syncWorkingIndicator = function() {
36112
36229
  if (!spinnerActive()) {
36113
36230
  if (workingSpinnerTimer) {
@@ -36136,7 +36253,7 @@ async function createSessionTui({
36136
36253
  slashCompletions = [];
36137
36254
  return;
36138
36255
  }
36139
- slashCompletions = composerSlashCommands.filter((command) => command.value.startsWith(value) && command.value !== value && (command.value !== "/complete" || latestState.aggregateStatus === "open"));
36256
+ slashCompletions = composerSlashCommands.filter((command) => command.value.startsWith(value) && command.value !== value && (!["/complete", "/cancel"].includes(command.value) || latestState.aggregateStatus === "open"));
36140
36257
  slashCompletionIndex = 0;
36141
36258
  }, finish = function(result, error93) {
36142
36259
  if (settled)
@@ -36178,8 +36295,9 @@ async function createSessionTui({
36178
36295
  let pendingPastedImageWrites = 0;
36179
36296
  let admittedSubmissions = [];
36180
36297
  let stopState = { kind: "idle" };
36181
- let completeState = { kind: "idle" };
36298
+ let lifecycleRequestState = { kind: "idle" };
36182
36299
  let helpVisible = false;
36300
+ let logoutConfirmationOpen = false;
36183
36301
  let latestState = controller.getState();
36184
36302
  let spinnerFrameIndex = 0;
36185
36303
  let renderedTranscript;
@@ -36236,6 +36354,17 @@ async function createSessionTui({
36236
36354
  return true;
36237
36355
  };
36238
36356
  keyHandler = (key) => {
36357
+ if (logoutConfirmationOpen) {
36358
+ key.preventDefault();
36359
+ if (key.name === "y") {
36360
+ finish({ kind: "logout" });
36361
+ } else if (key.name === "escape") {
36362
+ logoutConfirmationOpen = false;
36363
+ composerFeedback = undefined;
36364
+ render();
36365
+ }
36366
+ return;
36367
+ }
36239
36368
  if (key.ctrl && key.name === "c") {
36240
36369
  key.preventDefault();
36241
36370
  finish({ kind: "exit", signal: "SIGINT" });
@@ -36311,6 +36440,10 @@ async function createSessionTui({
36311
36440
  }
36312
36441
  }
36313
36442
  composer.onKeyDown = (key) => {
36443
+ if (logoutConfirmationOpen) {
36444
+ key.preventDefault();
36445
+ return;
36446
+ }
36314
36447
  if (slashCompletions.length > 0 && (key.name === "up" || key.name === "down")) {
36315
36448
  key.preventDefault();
36316
36449
  slashCompletionIndex = (slashCompletionIndex + (key.name === "up" ? -1 : 1) + slashCompletions.length) % slashCompletions.length;
@@ -36400,8 +36533,8 @@ async function createSessionTui({
36400
36533
  return;
36401
36534
  if (await handleComposerCommand(value))
36402
36535
  return;
36403
- if (completeState.kind === "completing") {
36404
- composerFeedback = "Completing the session \u2014 messages are paused.";
36536
+ if (lifecycleRequestState.kind === "pending") {
36537
+ composerFeedback = `${lifecycleOperationPresentParticiple(lifecycleRequestState.operation)} the session \u2014 messages are paused.`;
36405
36538
  render();
36406
36539
  return;
36407
36540
  }
@@ -36461,7 +36594,21 @@ async function createSessionTui({
36461
36594
  if (command === "/complete") {
36462
36595
  composer.setText("");
36463
36596
  composerDraft = { ...composerDraft, text: "" };
36464
- await handleCompleteRequest();
36597
+ await handleLifecycleRequest("complete");
36598
+ return true;
36599
+ }
36600
+ if (command === "/cancel") {
36601
+ composer.setText("");
36602
+ composerDraft = { ...composerDraft, text: "" };
36603
+ await handleLifecycleRequest("cancel");
36604
+ return true;
36605
+ }
36606
+ if (command === "/logout") {
36607
+ composer.setText("");
36608
+ composerDraft = { ...composerDraft, text: "" };
36609
+ logoutConfirmationOpen = true;
36610
+ composerFeedback = "Log out of Remy? Press y to confirm or Escape to cancel.";
36611
+ render();
36465
36612
  return true;
36466
36613
  }
36467
36614
  if (command === "/help") {
@@ -36537,39 +36684,39 @@ async function createSessionTui({
36537
36684
  }
36538
36685
  render();
36539
36686
  }
36540
- async function handleCompleteRequest() {
36541
- if (completeState.kind === "completing")
36687
+ async function handleLifecycleRequest(operation) {
36688
+ if (lifecycleRequestState.kind === "pending")
36542
36689
  return;
36543
36690
  if (latestState.aggregateStatus !== "open") {
36544
- completeState = { kind: "idle" };
36691
+ lifecycleRequestState = { kind: "idle" };
36545
36692
  composerFeedback = "This session is already terminal.";
36546
36693
  render();
36547
36694
  return;
36548
36695
  }
36549
- if (isRemyWorking(latestState)) {
36696
+ if (operation === "complete" && isRemyWorking(latestState)) {
36550
36697
  composerFeedback = "Stop Remy\u2019s turn before completing the session.";
36551
36698
  render();
36552
36699
  return;
36553
36700
  }
36554
- completeState = { kind: "completing" };
36555
- composerFeedback = "Completing the session\u2026";
36701
+ lifecycleRequestState = { kind: "pending", operation };
36702
+ composerFeedback = `${lifecycleOperationPresentParticiple(operation)} the session\u2026`;
36556
36703
  render();
36557
36704
  try {
36558
- await requestComplete();
36705
+ await (operation === "complete" ? requestComplete() : requestCancel());
36559
36706
  if (destroyed)
36560
36707
  return;
36561
- if (completeState.kind !== "completing")
36708
+ if (lifecycleRequestState.kind !== "pending" || lifecycleRequestState.operation !== operation)
36562
36709
  return;
36563
- composerFeedback = "Session completed.";
36710
+ composerFeedback = lifecycleOperationSuccessFeedback(operation);
36564
36711
  if (latestState.aggregateStatus !== "open")
36565
- completeState = { kind: "idle" };
36712
+ lifecycleRequestState = { kind: "idle" };
36566
36713
  } catch (error93) {
36567
36714
  if (destroyed)
36568
36715
  return;
36569
- if (completeState.kind === "completing") {
36716
+ if (lifecycleRequestState.kind === "pending" && lifecycleRequestState.operation === operation) {
36570
36717
  const message = error93 instanceof Error ? error93.message : String(error93);
36571
- completeState = { kind: "failed", message };
36572
- composerFeedback = `Could not complete the session: ${message}`;
36718
+ lifecycleRequestState = { kind: "failed", operation, message };
36719
+ composerFeedback = `Could not ${operation} the session: ${message}`;
36573
36720
  }
36574
36721
  }
36575
36722
  render();
@@ -36581,21 +36728,22 @@ async function createSessionTui({
36581
36728
  if (composerFeedback === "Stop requested.")
36582
36729
  composerFeedback = undefined;
36583
36730
  }
36584
- if (completeState.kind === "completing" && latestState.aggregateStatus !== "open") {
36585
- completeState = { kind: "idle" };
36586
- composerFeedback = "Session completed.";
36731
+ if (lifecycleRequestState.kind === "pending" && latestState.aggregateStatus !== "open") {
36732
+ const operation = lifecycleRequestState.operation;
36733
+ lifecycleRequestState = { kind: "idle" };
36734
+ composerFeedback = lifecycleOperationSuccessFeedback(operation);
36587
36735
  }
36588
36736
  render();
36589
36737
  });
36590
36738
  renderer.addInputHandler(inputHandler);
36591
36739
  renderer.keyInput.on("keypress", keyHandler);
36592
36740
  composer.setText(composerDraft.text);
36593
- renderer.on(CliRenderEvents3.RENDER_ERROR, (event) => finish(undefined, event.error));
36741
+ renderer.on(CliRenderEvents4.RENDER_ERROR, (event) => finish(undefined, event.error));
36594
36742
  resizeHandler = () => {
36595
36743
  if (!destroyed)
36596
36744
  render();
36597
36745
  };
36598
- renderer.on(CliRenderEvents3.RESIZE, resizeHandler);
36746
+ renderer.on(CliRenderEvents4.RESIZE, resizeHandler);
36599
36747
  render();
36600
36748
  if (latestState.aggregateStatus === "open")
36601
36749
  composer.focus();
@@ -36615,7 +36763,7 @@ async function createSessionTui({
36615
36763
  if (keyHandler)
36616
36764
  renderer.keyInput.off("keypress", keyHandler);
36617
36765
  if (resizeHandler)
36618
- renderer.off(CliRenderEvents3.RESIZE, resizeHandler);
36766
+ renderer.off(CliRenderEvents4.RESIZE, resizeHandler);
36619
36767
  composerLayout.destroy();
36620
36768
  rendererDestroyed2 = true;
36621
36769
  renderer.destroy();
@@ -36632,7 +36780,7 @@ async function createSessionTui({
36632
36780
  if (keyHandler)
36633
36781
  renderer.keyInput.off("keypress", keyHandler);
36634
36782
  if (resizeHandler)
36635
- renderer.off(CliRenderEvents3.RESIZE, resizeHandler);
36783
+ renderer.off(CliRenderEvents4.RESIZE, resizeHandler);
36636
36784
  if (!rendererDestroyed2) {
36637
36785
  rendererDestroyed2 = true;
36638
36786
  renderer.destroy();
@@ -36660,11 +36808,12 @@ var helpEntries = [
36660
36808
  { group: "command", token: "/new", description: "start a new session" },
36661
36809
  { group: "command", token: "/sessions", description: "back to the session list" },
36662
36810
  { group: "command", token: "/complete", description: "finish this session" },
36811
+ { group: "command", token: "/cancel", description: "cancel this session" },
36812
+ { group: "command", token: "/logout", description: "sign out of Remy" },
36663
36813
  { group: "command", token: "/exit", description: "leave Remy" },
36664
36814
  { group: "key", token: "esc", description: "stop Remy\u2019s current turn" },
36665
36815
  { group: "key", token: "ctrl+o", description: "expand the activity trail" },
36666
- { group: "key", token: "ctrl+r", description: "retry the oldest failed message" },
36667
- { group: "key", token: "ctrl+c", description: "quit" }
36816
+ { group: "key", token: "ctrl+r", description: "retry the oldest failed message" }
36668
36817
  ];
36669
36818
  function renderHelpPanel() {
36670
36819
  const width = helpEntries.reduce((max, entry) => Math.max(max, entry.token.length), 0) + 2;
@@ -36685,10 +36834,10 @@ function renderHelpPanel() {
36685
36834
  function hasRunningTurn(state) {
36686
36835
  return state.aggregateStatus === "open" && Object.values(state.messageTurns).some((turn) => turn.outcome === undefined);
36687
36836
  }
36688
- function renderStatusBand({ state, working, elapsedMs, stopState, completeState, repositoryLabel }) {
36837
+ function renderStatusBand({ state, working, elapsedMs, stopState, lifecycleRequestState, repositoryLabel }) {
36689
36838
  const stopping = stopState?.kind === "stopping";
36690
- const completing = completeState?.kind === "completing";
36691
- const presentation = completing ? { label: "Completing", color: PALETTE.approvalQuestion } : stopping ? { label: "Stopping", color: PALETTE.approvalQuestion } : sessionViewStatePresentation({ aggregateStatus: state.aggregateStatus, isWorking: working });
36839
+ const pendingOperation = lifecycleRequestState?.kind === "pending" ? lifecycleRequestState.operation : undefined;
36840
+ const presentation = pendingOperation ? { label: lifecycleOperationPresentParticiple(pendingOperation), color: PALETTE.approvalQuestion } : stopping ? { label: "Stopping", color: PALETTE.approvalQuestion } : sessionViewStatePresentation({ aggregateStatus: state.aggregateStatus, isWorking: working });
36692
36841
  const elapsed = (working || stopping) && elapsedMs !== undefined && elapsedMs > 0 ? ` ${formatElapsed(elapsedMs)}` : "";
36693
36842
  const connection = state.connectionStatus === "connected" ? dim4(fg6(PALETTE.dimText)("connected")) : fg6(PALETTE.approvalQuestion)("reconnecting\u2026");
36694
36843
  const sessionLabel = state.sessionNumber !== undefined ? `Session #${state.sessionNumber}` : `Session ${state.sessionId}`;
@@ -36722,14 +36871,14 @@ function renderTerminalSessionBand({ aggregateStatus }) {
36722
36871
  ], `
36723
36872
  `);
36724
36873
  }
36725
- function renderActionBar({ state, working, stopState, completeState }) {
36726
- if (completeState.kind === "completing")
36727
- return new StyledText5([dim4(fg6(PALETTE.dimText)("completing the session\u2026 \xB7 ctrl+c exits"))]);
36874
+ function renderActionBar({ state, working, stopState, lifecycleRequestState }) {
36875
+ if (lifecycleRequestState.kind === "pending")
36876
+ return new StyledText5([dim4(fg6(PALETTE.dimText)(`${lifecycleOperationPresentParticiple(lifecycleRequestState.operation).toLowerCase()} the session\u2026`))]);
36728
36877
  if (state.aggregateStatus !== "open")
36729
36878
  return new StyledText5([]);
36730
36879
  const stopToken = working && stopState.kind === "failed" ? ["esc retry stop"] : [];
36731
- const idleGroup = completeState.kind === "failed" ? "/complete retries \xB7 /new \xB7 /sessions" : "/new \xB7 /sessions \xB7 /complete";
36732
- const tokens = working ? ["/ commands", "/help", ...stopToken, "ctrl+o activity"] : ["/ commands", "/help", idleGroup, "ctrl+o activity"];
36880
+ const idleGroup = lifecycleRequestState.kind === "failed" ? `/${lifecycleRequestState.operation} retries \xB7 /new \xB7 /sessions` : "/new \xB7 /sessions \xB7 /complete \xB7 /cancel";
36881
+ const tokens = working ? ["/ commands", ...stopToken, "ctrl+o activity"] : ["/ commands", idleGroup, "ctrl+o activity"];
36733
36882
  return new StyledText5([dim4(fg6(PALETTE.dimText)(tokens.join(" \xB7 ")))]);
36734
36883
  }
36735
36884
  function isRemyWorking(state) {
@@ -36890,10 +37039,10 @@ function renderWorkingIndicator({ frame, elapsedMs, mode }) {
36890
37039
  dim4(fg6(PALETTE.dimText)(` Stopping\u2026 ${formatElapsed(elapsedMs)}`))
36891
37040
  ]);
36892
37041
  }
36893
- if (mode === "completing") {
37042
+ if (mode === "complete" || mode === "cancel") {
36894
37043
  return new StyledText5([
36895
37044
  fg6(PALETTE.approvalQuestion)(frame),
36896
- dim4(fg6(PALETTE.dimText)(" Completing the session\u2026"))
37045
+ dim4(fg6(PALETTE.dimText)(` ${lifecycleOperationPresentParticiple(mode)} the session\u2026`))
36897
37046
  ]);
36898
37047
  }
36899
37048
  return new StyledText5([
@@ -36901,6 +37050,12 @@ function renderWorkingIndicator({ frame, elapsedMs, mode }) {
36901
37050
  dim4(fg6(PALETTE.dimText)(` Working ${formatElapsed(elapsedMs)}`))
36902
37051
  ]);
36903
37052
  }
37053
+ function lifecycleOperationPresentParticiple(operation) {
37054
+ return operation === "complete" ? "Completing" : "Cancelling";
37055
+ }
37056
+ function lifecycleOperationSuccessFeedback(operation) {
37057
+ return operation === "complete" ? "Session completed." : "Session cancelled.";
37058
+ }
36904
37059
  function formatElapsed(elapsedMs) {
36905
37060
  const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
36906
37061
  if (totalSeconds < 60)
@@ -37042,7 +37197,7 @@ var compactMarkRows = 9;
37042
37197
  var compactMinWidth = 48;
37043
37198
  var compactMinHeight = 20;
37044
37199
  var markBrightnessGain = 4.2;
37045
- var remyCliVersion = "1.1.1";
37200
+ var remyCliVersion = "1.3.0";
37046
37201
  async function showRemySplash({
37047
37202
  createRenderer = createRemyRenderer,
37048
37203
  durationMs = splashDurationMs,
@@ -37224,7 +37379,7 @@ Verification URL: `), bold5(fg7(PALETTE.humanAccent)(state.verificationUrl))] :
37224
37379
  fg7(PALETTE.bodyText)("Continue authentication in your browser."),
37225
37380
  ...url3,
37226
37381
  fg7(PALETTE.dimText)(`
37227
- ${spinner} Waiting for approval \xB7 q / esc / ctrl+c cancel`)
37382
+ ${spinner} Waiting for approval \xB7 q / esc cancel`)
37228
37383
  ]);
37229
37384
  }
37230
37385
  async function waitForSplashAbort(signal) {
@@ -37517,7 +37672,7 @@ ${approvalUrl}
37517
37672
  `);
37518
37673
  });
37519
37674
  if (!splashPresented)
37520
- dependencies.output.writeStderr(`Waiting for approval\u2026 Press Ctrl+C to cancel.
37675
+ dependencies.output.writeStderr(`Waiting for approval\u2026
37521
37676
  `);
37522
37677
  });
37523
37678
  let journalPrepared = false;
@@ -37681,7 +37836,6 @@ function parseOptionalStringFlag(value, name) {
37681
37836
  }
37682
37837
  async function createAuthenticatedClient(dependencies) {
37683
37838
  const paths = resolveCliPaths(dependencies.environment);
37684
- const candidateTokenPath = await createCandidateTokenPath(paths.tokenPath);
37685
37839
  const snapshot = await withCredentialState({
37686
37840
  stateDirectory: dirname6(paths.tokenPath),
37687
37841
  action: async () => {
@@ -37693,43 +37847,21 @@ async function createAuthenticatedClient(dependencies) {
37693
37847
  });
37694
37848
  const configuration = await readCliConfiguration(paths.configPath);
37695
37849
  const tokenGeneration = await readOptionalTokenFile(paths.tokenPath);
37696
- if (tokenGeneration !== undefined)
37697
- await writeFile4(candidateTokenPath, tokenGeneration, { mode: 384 });
37698
37850
  return { configuration, tokenGeneration };
37699
37851
  }
37700
37852
  });
37701
- const candidateAuthConfiguration = toAuthConfiguration({ configuration: snapshot.configuration, tokenPath: candidateTokenPath });
37702
- const identity = await loadStoredIdentity(candidateAuthConfiguration);
37853
+ const authConfiguration = toAuthConfiguration({ configuration: snapshot.configuration, tokenPath: paths.tokenPath });
37854
+ const identity = await loadStoredIdentity(authConfiguration);
37855
+ const getAccessToken = createRefreshingAccessTokenProvider({ dependencies, paths, snapshot });
37703
37856
  try {
37704
- const accessToken = await requireAccessToken(candidateAuthConfiguration, dependencies.abortSignal);
37705
- await withCredentialState({
37706
- stateDirectory: dirname6(paths.tokenPath),
37707
- action: async () => {
37708
- await recoverCredentialPromotionWhileLocked({
37709
- stateDirectory: dirname6(paths.tokenPath),
37710
- configPath: paths.configPath,
37711
- tokenPath: paths.tokenPath,
37712
- ...dependencies.credentialStateFilesystem
37713
- });
37714
- await assertCredentialSnapshotCurrent({ paths, snapshot });
37715
- const candidateGeneration = await readFile5(candidateTokenPath, "utf8");
37716
- if (candidateGeneration !== snapshot.tokenGeneration) {
37717
- await replaceCredentialTokenWhileLocked({
37718
- sourcePath: candidateTokenPath,
37719
- tokenPath: paths.tokenPath,
37720
- renameFile: dependencies.credentialStateFilesystem?.renameFile,
37721
- syncDirectory: dependencies.credentialStateFilesystem?.syncDirectory
37722
- });
37723
- }
37724
- }
37725
- });
37857
+ await getAccessToken(dependencies.abortSignal);
37726
37858
  const client = createCodingAgentClient({
37727
37859
  apiUrl: snapshot.configuration.apiUrl,
37728
- getAccessToken: async () => accessToken
37860
+ getAccessToken
37729
37861
  });
37730
37862
  const remoteIdentity = await materializeRemoteCurrentUser({ client, signal: dependencies.abortSignal });
37731
37863
  return {
37732
- authConfiguration: toAuthConfiguration({ configuration: snapshot.configuration, tokenPath: paths.tokenPath }),
37864
+ authConfiguration,
37733
37865
  client,
37734
37866
  identity,
37735
37867
  authenticatedEmail: remoteIdentity.email
@@ -37755,6 +37887,85 @@ async function createAuthenticatedClient(dependencies) {
37755
37887
  });
37756
37888
  }
37757
37889
  throw error93;
37890
+ }
37891
+ }
37892
+ function createRefreshingAccessTokenProvider({
37893
+ dependencies,
37894
+ paths,
37895
+ snapshot
37896
+ }) {
37897
+ let tokenGeneration = snapshot.tokenGeneration;
37898
+ let pendingAccessToken;
37899
+ return async (signal) => {
37900
+ if (!pendingAccessToken) {
37901
+ const expectedTokenGeneration = tokenGeneration;
37902
+ pendingAccessToken = refreshAccessToken({ dependencies, paths, configuration: snapshot.configuration, expectedTokenGeneration, signal }).then(({ accessToken: accessToken2, tokenGeneration: refreshedTokenGeneration }) => {
37903
+ tokenGeneration = refreshedTokenGeneration;
37904
+ return accessToken2;
37905
+ });
37906
+ }
37907
+ const accessToken = pendingAccessToken;
37908
+ try {
37909
+ return await accessToken;
37910
+ } finally {
37911
+ if (pendingAccessToken === accessToken)
37912
+ pendingAccessToken = undefined;
37913
+ }
37914
+ };
37915
+ }
37916
+ async function refreshAccessToken({
37917
+ dependencies,
37918
+ paths,
37919
+ configuration,
37920
+ expectedTokenGeneration,
37921
+ signal
37922
+ }) {
37923
+ const candidateTokenPath = await createCandidateTokenPath(paths.tokenPath);
37924
+ try {
37925
+ await withCredentialState({
37926
+ stateDirectory: dirname6(paths.tokenPath),
37927
+ action: async () => {
37928
+ await recoverCredentialPromotionWhileLocked({
37929
+ stateDirectory: dirname6(paths.tokenPath),
37930
+ configPath: paths.configPath,
37931
+ tokenPath: paths.tokenPath,
37932
+ ...dependencies.credentialStateFilesystem
37933
+ });
37934
+ await assertCredentialSnapshotCurrent({
37935
+ paths,
37936
+ snapshot: { configuration, tokenGeneration: expectedTokenGeneration }
37937
+ });
37938
+ if (expectedTokenGeneration !== undefined)
37939
+ await writeFile4(candidateTokenPath, expectedTokenGeneration, { mode: 384 });
37940
+ }
37941
+ });
37942
+ const candidateAuthConfiguration = toAuthConfiguration({ configuration, tokenPath: candidateTokenPath });
37943
+ const accessToken = await requireAccessToken(candidateAuthConfiguration, signal);
37944
+ const candidateTokenGeneration = await readFile5(candidateTokenPath, "utf8");
37945
+ await withCredentialState({
37946
+ stateDirectory: dirname6(paths.tokenPath),
37947
+ action: async () => {
37948
+ await recoverCredentialPromotionWhileLocked({
37949
+ stateDirectory: dirname6(paths.tokenPath),
37950
+ configPath: paths.configPath,
37951
+ tokenPath: paths.tokenPath,
37952
+ ...dependencies.credentialStateFilesystem
37953
+ });
37954
+ await assertCredentialSnapshotCurrent({
37955
+ paths,
37956
+ snapshot: { configuration, tokenGeneration: expectedTokenGeneration }
37957
+ });
37958
+ if (candidateTokenGeneration !== expectedTokenGeneration) {
37959
+ await replaceCredentialTokenWhileLocked({
37960
+ sourcePath: candidateTokenPath,
37961
+ tokenPath: paths.tokenPath,
37962
+ renameFile: dependencies.credentialStateFilesystem?.renameFile,
37963
+ syncDirectory: dependencies.credentialStateFilesystem?.syncDirectory
37964
+ });
37965
+ }
37966
+ }
37967
+ });
37968
+ return { accessToken, tokenGeneration: candidateTokenGeneration };
37758
37969
  } finally {
37759
37970
  await removeCandidateToken({ path: candidateTokenPath, removeTokenFile: unlink3 });
37760
37971
  }
@@ -37847,6 +38058,8 @@ async function dashboard({
37847
38058
  return 0;
37848
38059
  if (action.kind === "interrupt")
37849
38060
  throw new CliInterruptedError("SIGINT");
38061
+ if (action.kind === "logout")
38062
+ return await logout({ dependencies, flags: {} });
37850
38063
  holdTransitionScreen();
37851
38064
  if (action.kind === "session")
37852
38065
  return await attachSession({ dependencies, sessionId: action.sessionId, noTui: false, json: false });
@@ -38326,6 +38539,10 @@ async function runAttachedSession({
38326
38539
  const detail = await operations.completeSession({ client: operations.client, sessionId });
38327
38540
  controller.updateDetail(detail);
38328
38541
  },
38542
+ requestCancel: async () => {
38543
+ const detail = await operations.cancelSession({ client: operations.client, sessionId });
38544
+ controller.updateDetail(detail);
38545
+ },
38329
38546
  readClipboardImage,
38330
38547
  savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir() }),
38331
38548
  attachPromptPaths: async (input) => await uploadPromptPaths({ operations, ...input })
@@ -38354,6 +38571,13 @@ async function runAttachedSession({
38354
38571
  bootstrap: await prepareDashboardBootstrap({ dependencies, operations })
38355
38572
  });
38356
38573
  }
38574
+ if (action.kind === "logout") {
38575
+ stop();
38576
+ await startPromise;
38577
+ if (dependencies.abortSignal?.aborted)
38578
+ throw dependencies.abortSignal.reason ?? new Error("interrupted");
38579
+ return await logout({ dependencies, flags: {} });
38580
+ }
38357
38581
  holdTransitionScreen();
38358
38582
  const availableRepositories = await collectAllRepositories({ listRepositories: operations.listRepositories });
38359
38583
  let draft = await openTuiNewSessionWizard({ dependencies, operations, repositories: availableRepositories });
@@ -38433,6 +38657,7 @@ async function createSessionOperations(dependencies, { onAuthenticated } = {}) {
38433
38657
  reserveAndUploadFile: dependencies.reserveAndUploadFile ?? reserveAndUploadFile,
38434
38658
  createCodexSession: dependencies.createCodexSession ?? createCodexSession,
38435
38659
  appendSessionMessage: dependencies.appendSessionMessage ?? appendSessionMessage,
38660
+ cancelSession: dependencies.cancelSession ?? cancelSession,
38436
38661
  completeSession: dependencies.completeSession ?? completeSession,
38437
38662
  interruptSession: dependencies.interruptSession ?? interruptSession,
38438
38663
  getSession: dependencies.getSession ?? getSession,
@@ -38450,6 +38675,7 @@ async function createSessionOperations(dependencies, { onAuthenticated } = {}) {
38450
38675
  reserveAndUploadFile: dependencies.reserveAndUploadFile ?? reserveAndUploadFile,
38451
38676
  createCodexSession: dependencies.createCodexSession ?? createCodexSession,
38452
38677
  appendSessionMessage: dependencies.appendSessionMessage ?? appendSessionMessage,
38678
+ cancelSession: dependencies.cancelSession ?? cancelSession,
38453
38679
  completeSession: dependencies.completeSession ?? completeSession,
38454
38680
  interruptSession: dependencies.interruptSession ?? interruptSession,
38455
38681
  getSession: dependencies.getSession ?? getSession,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/remy-cli",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "Remy, the Coding Agent terminal client.",
5
5
  "type": "module",
6
6
  "bin": {