@meistrari/remy-cli 1.1.1 → 1.2.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 +6 -3
  2. package/dist/remy.js +272 -75
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -42,7 +42,9 @@ 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
  | --- | --- |
@@ -50,11 +52,12 @@ Use the arrow keys to select a session in the dashboard and press `Enter` to ope
50
52
  | `/complete` | Complete an open session after its active turn stops. |
51
53
  | `/sessions` | Return to the dashboard. |
52
54
  | `/new` | Start another new-session flow. |
55
+ | `/logout` | Sign out of Remy after confirmation. |
53
56
  | `/help` | Show session controls. |
54
57
  | `/exit` | Leave Remy. |
55
58
  | `Ctrl+O` | Expand or collapse activity details. |
56
59
 
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.
60
+ 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
61
 
59
62
  ## Commands for direct or scripted use
60
63
 
@@ -112,7 +115,7 @@ remy login \
112
115
  --target-application-id <target-application-uuid>
113
116
  ```
114
117
 
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.
118
+ 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
119
 
117
120
  ## Complete command reference
118
121
 
package/dist/remy.js CHANGED
@@ -34355,7 +34355,7 @@ async function sleepMs(ms, signal) {
34355
34355
  }
34356
34356
 
34357
34357
  // src/tui/dashboard.ts
34358
- import { bg, BoxRenderable, bold as bold2, CliRenderEvents, fg as fg3, InputRenderable, ScrollBoxRenderable, stringToStyledText as stringToStyledText2, TextRenderable } from "@opentui/core";
34358
+ import { bg, BoxRenderable, bold as bold2, CliRenderEvents as CliRenderEvents2, fg as fg3, ScrollBoxRenderable, stringToStyledText as stringToStyledText2, TextRenderable } from "@opentui/core";
34359
34359
 
34360
34360
  // src/tui/composer-divider.ts
34361
34361
  import { dim as dim2, fg as fg2, StyledText as StyledText2 } from "@opentui/core";
@@ -34551,18 +34551,20 @@ function renderComposerDivider({ width, tag, leadLabel }) {
34551
34551
  }
34552
34552
 
34553
34553
  // src/tui/renderer.ts
34554
- import { createCliRenderer } from "@opentui/core";
34554
+ import { CliRenderEvents, createClipboard, createCliRenderer, createHostClipboard, createRendererClipboardAdapter } from "@opentui/core";
34555
34555
  var rendererDestroyed = new WeakMap;
34556
34556
  var enterAlternateScreen = "\x1B[?1049h";
34557
34557
  var clearScreen = "\x1B[2J\x1B[H";
34558
34558
  var exitAlternateScreen = "\x1B[?1049l";
34559
34559
  var transitionScreenWriter;
34560
+ var ignoreTerminalFocus = (sequence) => sequence === "\x1B[I" || sequence === "\x1B[O";
34560
34561
  async function createRemyRenderer({
34561
34562
  createRenderer = createCliRenderer
34562
34563
  } = {}) {
34563
34564
  let resolveDestroyed = () => {
34564
34565
  return;
34565
34566
  };
34567
+ let selectionClipboard;
34566
34568
  const destroyed = new Promise((resolve) => {
34567
34569
  resolveDestroyed = resolve;
34568
34570
  });
@@ -34571,9 +34573,34 @@ async function createRemyRenderer({
34571
34573
  exitSignals: [],
34572
34574
  screenMode: "alternate-screen",
34573
34575
  useMouse: true,
34574
- onDestroy: resolveDestroyed
34576
+ prependInputHandlers: [ignoreTerminalFocus],
34577
+ onDestroy: () => {
34578
+ selectionClipboard?.dispose().catch(() => {
34579
+ return;
34580
+ });
34581
+ resolveDestroyed();
34582
+ }
34575
34583
  });
34576
34584
  transitionScreenWriter = undefined;
34585
+ try {
34586
+ selectionClipboard = createClipboard({
34587
+ host: createHostClipboard(),
34588
+ terminal: createRendererClipboardAdapter(renderer)
34589
+ });
34590
+ } catch {}
34591
+ renderer.on(CliRenderEvents.SELECTION, (selection) => {
34592
+ const text = selection.getSelectedText();
34593
+ if (text.length === 0)
34594
+ return;
34595
+ renderer.clearSelection();
34596
+ if (selectionClipboard) {
34597
+ selectionClipboard.writeText(text, { destination: "all-available" }).catch(() => {
34598
+ return;
34599
+ });
34600
+ return;
34601
+ }
34602
+ renderer.copyToClipboardOSC52(text);
34603
+ });
34577
34604
  rendererDestroyed.set(renderer, destroyed);
34578
34605
  return renderer;
34579
34606
  }
@@ -34611,6 +34638,7 @@ async function createDashboardTui({
34611
34638
  }) {
34612
34639
  const renderer = await createRenderer();
34613
34640
  let inputHandler;
34641
+ let keyHandler;
34614
34642
  let resizeHandler;
34615
34643
  let rendererDestroyed2 = false;
34616
34644
  try {
@@ -34637,8 +34665,7 @@ async function createDashboardTui({
34637
34665
  previewBand.visible = showPreview;
34638
34666
  previewBand.content = showPreview ? formatSessionPreview({ session: selectedSession, width: contentWidth }) : stringToStyledText2("");
34639
34667
  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";
34668
+ footer.content = dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen });
34642
34669
  renderer.requestRender();
34643
34670
  }, finish = function(value) {
34644
34671
  if (settled)
@@ -34650,6 +34677,11 @@ async function createDashboardTui({
34650
34677
  return;
34651
34678
  settled = true;
34652
34679
  rejectDraft(error93);
34680
+ }, moveSelectionTo = function(index) {
34681
+ if (page.sessions.length === 0)
34682
+ return;
34683
+ selectedIndex = Math.max(0, Math.min(index, page.sessions.length - 1));
34684
+ render();
34653
34685
  }, handleSessionKey = function(key) {
34654
34686
  if (logoutConfirmationOpen) {
34655
34687
  key.preventDefault();
@@ -34661,16 +34693,57 @@ async function createDashboardTui({
34661
34693
  }
34662
34694
  return;
34663
34695
  }
34696
+ const listFocused = renderer.currentFocusedRenderable === listRegion;
34664
34697
  if (key.name === "down") {
34665
34698
  key.preventDefault();
34699
+ awaitingVimGoToTop = false;
34666
34700
  moveSelection("next");
34667
34701
  return;
34668
34702
  }
34669
34703
  if (key.name === "up") {
34670
34704
  key.preventDefault();
34705
+ awaitingVimGoToTop = false;
34671
34706
  moveSelection("previous");
34672
34707
  return;
34673
34708
  }
34709
+ if (key.name === "right") {
34710
+ key.preventDefault();
34711
+ awaitingVimGoToTop = false;
34712
+ loadAdjacentPage("next");
34713
+ return;
34714
+ }
34715
+ if (key.name === "left") {
34716
+ key.preventDefault();
34717
+ awaitingVimGoToTop = false;
34718
+ loadAdjacentPage("previous");
34719
+ return;
34720
+ }
34721
+ if (listFocused && (key.name === "G" || key.name === "g" && key.shift)) {
34722
+ key.preventDefault();
34723
+ awaitingVimGoToTop = false;
34724
+ moveSelectionTo(page.sessions.length - 1);
34725
+ return;
34726
+ }
34727
+ if (listFocused && key.name === "g") {
34728
+ key.preventDefault();
34729
+ if (awaitingVimGoToTop)
34730
+ moveSelectionTo(0);
34731
+ awaitingVimGoToTop = !awaitingVimGoToTop;
34732
+ return;
34733
+ }
34734
+ if (listFocused && (key.name === "j" || key.name === "k")) {
34735
+ key.preventDefault();
34736
+ awaitingVimGoToTop = false;
34737
+ moveSelection(key.name === "j" ? "next" : "previous");
34738
+ return;
34739
+ }
34740
+ if (listFocused && !key.shift && (key.name === "h" || key.name === "l")) {
34741
+ key.preventDefault();
34742
+ awaitingVimGoToTop = false;
34743
+ loadAdjacentPage(key.name === "l" ? "next" : "previous");
34744
+ return;
34745
+ }
34746
+ awaitingVimGoToTop = false;
34674
34747
  if (key.name === "return" || key.name === "enter") {
34675
34748
  key.preventDefault();
34676
34749
  const session = page.sessions[selectedIndex];
@@ -34683,7 +34756,7 @@ async function createDashboardTui({
34683
34756
  finish({ kind: "new" });
34684
34757
  return;
34685
34758
  }
34686
- if (key.name === "l") {
34759
+ if (key.name === "L" || key.shift && key.name === "l") {
34687
34760
  key.preventDefault();
34688
34761
  logoutConfirmationOpen = true;
34689
34762
  render();
@@ -34706,20 +34779,23 @@ async function createDashboardTui({
34706
34779
  const previewBand = new TextRenderable(renderer, { content: "", flexShrink: 0 });
34707
34780
  const separator = new TextRenderable(renderer, { content: "", flexShrink: 0 });
34708
34781
  const footer = new TextRenderable(renderer, { content: "", flexShrink: 0 });
34709
- const input = new InputRenderable(renderer, { placeholder: "Describe the work to do" });
34782
+ root.onMouseDown = (event) => {
34783
+ event.preventDefault();
34784
+ listRegion.focus();
34785
+ };
34710
34786
  listRegion.add(content);
34711
34787
  root.add(statusBand);
34712
34788
  root.add(listRegion);
34713
34789
  root.add(previewBand);
34714
34790
  root.add(separator);
34715
34791
  root.add(footer);
34716
- root.add(input);
34717
34792
  renderer.root.add(root);
34718
34793
  let page = initialPage;
34719
34794
  let pageIndex = 1;
34720
34795
  let selectedIndex = 0;
34721
34796
  let loadingPage = false;
34722
34797
  let pageError;
34798
+ let awaitingVimGoToTop = false;
34723
34799
  let logoutConfirmationOpen = false;
34724
34800
  let destroyed = false;
34725
34801
  let rendererDestroyPromise;
@@ -34734,16 +34810,10 @@ async function createDashboardTui({
34734
34810
  resolveAction = resolve;
34735
34811
  rejectDraft = reject;
34736
34812
  });
34737
- async function moveSelection(direction) {
34813
+ async function loadAdjacentPage(direction) {
34738
34814
  if (loadingPage)
34739
34815
  return;
34740
- const isBoundary = direction === "next" ? selectedIndex === page.sessions.length - 1 : selectedIndex === 0;
34741
34816
  const canLoad = direction === "next" ? page.hasMore : page.canGoPrevious;
34742
- if (!isBoundary) {
34743
- selectedIndex += direction === "next" ? 1 : -1;
34744
- render();
34745
- return;
34746
- }
34747
34817
  if (!canLoad)
34748
34818
  return;
34749
34819
  loadingPage = true;
@@ -34760,6 +34830,17 @@ async function createDashboardTui({
34760
34830
  render();
34761
34831
  }
34762
34832
  }
34833
+ async function moveSelection(direction) {
34834
+ if (loadingPage || page.sessions.length === 0)
34835
+ return;
34836
+ const isBoundary = direction === "next" ? selectedIndex === page.sessions.length - 1 : selectedIndex === 0;
34837
+ if (!isBoundary) {
34838
+ selectedIndex += direction === "next" ? 1 : -1;
34839
+ render();
34840
+ return;
34841
+ }
34842
+ await loadAdjacentPage(direction);
34843
+ }
34763
34844
  inputHandler = (sequence) => {
34764
34845
  if (sequence === "\x03") {
34765
34846
  finish({ kind: "interrupt" });
@@ -34767,16 +34848,17 @@ async function createDashboardTui({
34767
34848
  }
34768
34849
  return false;
34769
34850
  };
34770
- input.onKeyDown = handleSessionKey;
34851
+ keyHandler = handleSessionKey;
34771
34852
  renderer.addInputHandler(inputHandler);
34772
- renderer.on(CliRenderEvents.RENDER_ERROR, (event) => fail(event.error));
34853
+ renderer.keyInput.on("keypress", keyHandler);
34854
+ renderer.on(CliRenderEvents2.RENDER_ERROR, (event) => fail(event.error));
34773
34855
  resizeHandler = () => {
34774
34856
  if (!destroyed)
34775
34857
  render();
34776
34858
  };
34777
- renderer.on(CliRenderEvents.RESIZE, resizeHandler);
34778
- input.focus();
34859
+ renderer.on(CliRenderEvents2.RESIZE, resizeHandler);
34779
34860
  render();
34861
+ listRegion.focus();
34780
34862
  return {
34781
34863
  waitForAction: async () => await action,
34782
34864
  destroy() {
@@ -34785,8 +34867,10 @@ async function createDashboardTui({
34785
34867
  destroyed = true;
34786
34868
  if (inputHandler)
34787
34869
  renderer.removeInputHandler(inputHandler);
34870
+ if (keyHandler)
34871
+ renderer.keyInput.off("keypress", keyHandler);
34788
34872
  if (resizeHandler)
34789
- renderer.off(CliRenderEvents.RESIZE, resizeHandler);
34873
+ renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
34790
34874
  rendererDestroyed2 = true;
34791
34875
  renderer.destroy();
34792
34876
  },
@@ -34797,8 +34881,10 @@ async function createDashboardTui({
34797
34881
  } catch (error93) {
34798
34882
  if (inputHandler)
34799
34883
  renderer.removeInputHandler(inputHandler);
34884
+ if (keyHandler)
34885
+ renderer.keyInput.off("keypress", keyHandler);
34800
34886
  if (resizeHandler)
34801
- renderer.off(CliRenderEvents.RESIZE, resizeHandler);
34887
+ renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
34802
34888
  if (!rendererDestroyed2) {
34803
34889
  rendererDestroyed2 = true;
34804
34890
  renderer.destroy();
@@ -34811,22 +34897,24 @@ function formatSessionRow({ session, selected, width }) {
34811
34897
  const outcome = sessionOutcome(session);
34812
34898
  const updated = formatUpdatedAt(session.updatedAt ?? session.createdAt);
34813
34899
  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));
34900
+ const title = truncate(session.title ?? "Untitled session", Math.max(8, Math.min(40, Math.floor(width * 0.24))));
34901
+ const attribution = `${session.creator} (${session.source})`;
34902
+ const metadataWidth = Math.max(0, width - prefix.length - title.length - activity.label.length - 6);
34903
+ const metadata = truncate(`${attribution} \xB7 ${outcome} \xB7 ${updated}`, metadataWidth);
34815
34904
  const line = [
34816
34905
  fg3(PALETTE.dimText)(prefix),
34817
34906
  fg3(PALETTE.bodyText)(title),
34818
34907
  fg3(PALETTE.dimText)(" "),
34819
34908
  bold2(fg3(activity.color)(activity.label)),
34820
34909
  fg3(PALETTE.dimText)(" "),
34821
- fg3(PALETTE.bodyText)(outcome),
34822
- fg3(PALETTE.dimText)(` ${updated}`)
34910
+ fg3(PALETTE.dimText)(metadata)
34823
34911
  ];
34824
34912
  const lineLength = line.reduce((total, chunk) => total + chunk.text.length, 0);
34825
34913
  const padded = selected ? [...line, fg3(PALETTE.dimText)(" ".repeat(Math.max(0, width - lineLength)))] : line;
34826
34914
  return selected ? padded.map((chunk) => bg(PALETTE.selectionBg)(chunk)) : padded;
34827
34915
  }
34828
34916
  function visibleListWindow({ sessionCount, selectedIndex, height }) {
34829
- const rowCount = Math.max(1, height - 10);
34917
+ const rowCount = Math.max(1, height - 9);
34830
34918
  const firstIndex = Math.min(Math.max(0, selectedIndex - Math.floor(rowCount / 2)), Math.max(0, sessionCount - rowCount));
34831
34919
  return { firstIndex, rowCount };
34832
34920
  }
@@ -34929,7 +35017,9 @@ function dashboardStatusBand({ page, pageIndex }) {
34929
35017
  paging.push("more \u2192");
34930
35018
  return `remy \xB7 sessions \xB7 ${openCount} open \xB7 ${paging.join(" \xB7 ")}`;
34931
35019
  }
34932
- function dashboardFooter({ loadingPage, pageError, page, rowsBelow }) {
35020
+ function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen }) {
35021
+ if (logoutConfirmationOpen)
35022
+ return "Log out of Remy? y confirms \xB7 Esc cancels";
34933
35023
  const parts = [];
34934
35024
  if (loadingPage)
34935
35025
  parts.push("Loading sessions\u2026");
@@ -34937,9 +35027,7 @@ function dashboardFooter({ loadingPage, pageError, page, rowsBelow }) {
34937
35027
  parts.push(`Could not load sessions: ${pageError}`);
34938
35028
  if (rowsBelow > 0)
34939
35029
  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");
35030
+ parts.push("\u2191\u2193 move \xB7 \u23CE open \xB7 n new session \xB7 \u21E7l log out \xB7 q quit");
34943
35031
  return parts.join(" \xB7 ");
34944
35032
  }
34945
35033
  function truncate(value, width) {
@@ -34952,7 +35040,7 @@ async function createDefaultRenderer() {
34952
35040
  }
34953
35041
 
34954
35042
  // 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";
35043
+ 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
35044
 
34957
35045
  // src/tui/composer.ts
34958
35046
  import { BoxRenderable as BoxRenderable2, TextRenderable as TextRenderable2, TextareaRenderable } from "@opentui/core";
@@ -35297,7 +35385,7 @@ async function createNewSessionWizard({
35297
35385
  if (composerMounted)
35298
35386
  composer.render();
35299
35387
  if (repositoryListVisible) {
35300
- renderer.once(CliRenderEvents2.FRAME, () => {
35388
+ renderer.once(CliRenderEvents3.FRAME, () => {
35301
35389
  if (destroyed || step !== "repositories" && step !== "repositorySearch")
35302
35390
  return;
35303
35391
  const nextRepositoryHeaderHeight = Math.min(content.virtualLineCount, Math.floor(renderer.height * 0.45));
@@ -35526,6 +35614,24 @@ async function createNewSessionWizard({
35526
35614
  if (step !== "repositories")
35527
35615
  return;
35528
35616
  const choices = visibleRepositories();
35617
+ const repositoryListFocused = renderer.currentFocusedRenderable === repositoryScrollBox;
35618
+ if (repositoryListFocused && (key.name === "G" || key.name === "g" && key.shift)) {
35619
+ key.preventDefault();
35620
+ awaitingVimGoToTop = false;
35621
+ if (choices.length > 0)
35622
+ repositoryIndex = choices.length - 1;
35623
+ render();
35624
+ return;
35625
+ }
35626
+ if (repositoryListFocused && key.name === "g") {
35627
+ key.preventDefault();
35628
+ if (awaitingVimGoToTop && choices.length > 0)
35629
+ repositoryIndex = 0;
35630
+ awaitingVimGoToTop = !awaitingVimGoToTop;
35631
+ render();
35632
+ return;
35633
+ }
35634
+ awaitingVimGoToTop = false;
35529
35635
  if (key.name === "up" || key.name === "down" || key.name === "j" || key.name === "k") {
35530
35636
  key.preventDefault();
35531
35637
  if (choices.length > 0)
@@ -35599,6 +35705,7 @@ async function createNewSessionWizard({
35599
35705
  let completionIndex = 0;
35600
35706
  let pathCompletionMenuOpen = false;
35601
35707
  let repositoryHeaderHeight;
35708
+ let awaitingVimGoToTop = false;
35602
35709
  const manualSelections = new Map;
35603
35710
  let searchSelections;
35604
35711
  let suggestedIds = new Set;
@@ -35822,7 +35929,7 @@ async function createNewSessionWizard({
35822
35929
  };
35823
35930
  renderer.addInputHandler(inputHandler);
35824
35931
  renderer.keyInput.on("keypress", keyHandler);
35825
- renderer.on(CliRenderEvents2.RENDER_ERROR, (event) => fail(event.error));
35932
+ renderer.on(CliRenderEvents3.RENDER_ERROR, (event) => fail(event.error));
35826
35933
  resizeHandler = () => {
35827
35934
  if (destroyed)
35828
35935
  return;
@@ -35831,7 +35938,7 @@ async function createNewSessionWizard({
35831
35938
  repositoryIndex = Math.min(repositoryIndex, choices.length - 1);
35832
35939
  render();
35833
35940
  };
35834
- renderer.on(CliRenderEvents2.RESIZE, resizeHandler);
35941
+ renderer.on(CliRenderEvents3.RESIZE, resizeHandler);
35835
35942
  render();
35836
35943
  if (!Array.isArray(repositories))
35837
35944
  requestRepositoryLoad({ retry: false });
@@ -35846,7 +35953,7 @@ async function createNewSessionWizard({
35846
35953
  if (keyHandler)
35847
35954
  renderer.keyInput.off("keypress", keyHandler);
35848
35955
  if (resizeHandler)
35849
- renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
35956
+ renderer.off(CliRenderEvents3.RESIZE, resizeHandler);
35850
35957
  if (suggestionSpinnerTimer) {
35851
35958
  clearInterval(suggestionSpinnerTimer);
35852
35959
  suggestionSpinnerTimer = undefined;
@@ -35867,7 +35974,7 @@ async function createNewSessionWizard({
35867
35974
  if (keyHandler)
35868
35975
  renderer.keyInput.off("keypress", keyHandler);
35869
35976
  if (resizeHandler)
35870
- renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
35977
+ renderer.off(CliRenderEvents3.RESIZE, resizeHandler);
35871
35978
  if (!rendererDestroyed2) {
35872
35979
  rendererDestroyed2 = true;
35873
35980
  renderer.destroy();
@@ -35950,11 +36057,12 @@ async function createDefaultRenderer2() {
35950
36057
  }
35951
36058
 
35952
36059
  // 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";
36060
+ 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
36061
  var composerSlashCommands = [
35955
36062
  { value: "/sessions", description: "Back to the session list" },
35956
36063
  { value: "/complete", description: "Finish this session" },
35957
36064
  { value: "/new", description: "Start a new session" },
36065
+ { value: "/logout", description: "Sign out of Remy" },
35958
36066
  { value: "/help", description: "Show what you can do here" },
35959
36067
  { value: "/exit", description: "Leave Remy" }
35960
36068
  ];
@@ -36180,6 +36288,7 @@ async function createSessionTui({
36180
36288
  let stopState = { kind: "idle" };
36181
36289
  let completeState = { kind: "idle" };
36182
36290
  let helpVisible = false;
36291
+ let logoutConfirmationOpen = false;
36183
36292
  let latestState = controller.getState();
36184
36293
  let spinnerFrameIndex = 0;
36185
36294
  let renderedTranscript;
@@ -36236,6 +36345,17 @@ async function createSessionTui({
36236
36345
  return true;
36237
36346
  };
36238
36347
  keyHandler = (key) => {
36348
+ if (logoutConfirmationOpen) {
36349
+ key.preventDefault();
36350
+ if (key.name === "y") {
36351
+ finish({ kind: "logout" });
36352
+ } else if (key.name === "escape") {
36353
+ logoutConfirmationOpen = false;
36354
+ composerFeedback = undefined;
36355
+ render();
36356
+ }
36357
+ return;
36358
+ }
36239
36359
  if (key.ctrl && key.name === "c") {
36240
36360
  key.preventDefault();
36241
36361
  finish({ kind: "exit", signal: "SIGINT" });
@@ -36311,6 +36431,10 @@ async function createSessionTui({
36311
36431
  }
36312
36432
  }
36313
36433
  composer.onKeyDown = (key) => {
36434
+ if (logoutConfirmationOpen) {
36435
+ key.preventDefault();
36436
+ return;
36437
+ }
36314
36438
  if (slashCompletions.length > 0 && (key.name === "up" || key.name === "down")) {
36315
36439
  key.preventDefault();
36316
36440
  slashCompletionIndex = (slashCompletionIndex + (key.name === "up" ? -1 : 1) + slashCompletions.length) % slashCompletions.length;
@@ -36464,6 +36588,14 @@ async function createSessionTui({
36464
36588
  await handleCompleteRequest();
36465
36589
  return true;
36466
36590
  }
36591
+ if (command === "/logout") {
36592
+ composer.setText("");
36593
+ composerDraft = { ...composerDraft, text: "" };
36594
+ logoutConfirmationOpen = true;
36595
+ composerFeedback = "Log out of Remy? Press y to confirm or Escape to cancel.";
36596
+ render();
36597
+ return true;
36598
+ }
36467
36599
  if (command === "/help") {
36468
36600
  composer.setText("");
36469
36601
  composerDraft = { ...composerDraft, text: "" };
@@ -36590,12 +36722,12 @@ async function createSessionTui({
36590
36722
  renderer.addInputHandler(inputHandler);
36591
36723
  renderer.keyInput.on("keypress", keyHandler);
36592
36724
  composer.setText(composerDraft.text);
36593
- renderer.on(CliRenderEvents3.RENDER_ERROR, (event) => finish(undefined, event.error));
36725
+ renderer.on(CliRenderEvents4.RENDER_ERROR, (event) => finish(undefined, event.error));
36594
36726
  resizeHandler = () => {
36595
36727
  if (!destroyed)
36596
36728
  render();
36597
36729
  };
36598
- renderer.on(CliRenderEvents3.RESIZE, resizeHandler);
36730
+ renderer.on(CliRenderEvents4.RESIZE, resizeHandler);
36599
36731
  render();
36600
36732
  if (latestState.aggregateStatus === "open")
36601
36733
  composer.focus();
@@ -36615,7 +36747,7 @@ async function createSessionTui({
36615
36747
  if (keyHandler)
36616
36748
  renderer.keyInput.off("keypress", keyHandler);
36617
36749
  if (resizeHandler)
36618
- renderer.off(CliRenderEvents3.RESIZE, resizeHandler);
36750
+ renderer.off(CliRenderEvents4.RESIZE, resizeHandler);
36619
36751
  composerLayout.destroy();
36620
36752
  rendererDestroyed2 = true;
36621
36753
  renderer.destroy();
@@ -36632,7 +36764,7 @@ async function createSessionTui({
36632
36764
  if (keyHandler)
36633
36765
  renderer.keyInput.off("keypress", keyHandler);
36634
36766
  if (resizeHandler)
36635
- renderer.off(CliRenderEvents3.RESIZE, resizeHandler);
36767
+ renderer.off(CliRenderEvents4.RESIZE, resizeHandler);
36636
36768
  if (!rendererDestroyed2) {
36637
36769
  rendererDestroyed2 = true;
36638
36770
  renderer.destroy();
@@ -36660,11 +36792,11 @@ var helpEntries = [
36660
36792
  { group: "command", token: "/new", description: "start a new session" },
36661
36793
  { group: "command", token: "/sessions", description: "back to the session list" },
36662
36794
  { group: "command", token: "/complete", description: "finish this session" },
36795
+ { group: "command", token: "/logout", description: "sign out of Remy" },
36663
36796
  { group: "command", token: "/exit", description: "leave Remy" },
36664
36797
  { group: "key", token: "esc", description: "stop Remy\u2019s current turn" },
36665
36798
  { 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" }
36799
+ { group: "key", token: "ctrl+r", description: "retry the oldest failed message" }
36668
36800
  ];
36669
36801
  function renderHelpPanel() {
36670
36802
  const width = helpEntries.reduce((max, entry) => Math.max(max, entry.token.length), 0) + 2;
@@ -36724,7 +36856,7 @@ function renderTerminalSessionBand({ aggregateStatus }) {
36724
36856
  }
36725
36857
  function renderActionBar({ state, working, stopState, completeState }) {
36726
36858
  if (completeState.kind === "completing")
36727
- return new StyledText5([dim4(fg6(PALETTE.dimText)("completing the session\u2026 \xB7 ctrl+c exits"))]);
36859
+ return new StyledText5([dim4(fg6(PALETTE.dimText)("completing the session\u2026"))]);
36728
36860
  if (state.aggregateStatus !== "open")
36729
36861
  return new StyledText5([]);
36730
36862
  const stopToken = working && stopState.kind === "failed" ? ["esc retry stop"] : [];
@@ -37042,7 +37174,7 @@ var compactMarkRows = 9;
37042
37174
  var compactMinWidth = 48;
37043
37175
  var compactMinHeight = 20;
37044
37176
  var markBrightnessGain = 4.2;
37045
- var remyCliVersion = "1.1.1";
37177
+ var remyCliVersion = "1.2.0";
37046
37178
  async function showRemySplash({
37047
37179
  createRenderer = createRemyRenderer,
37048
37180
  durationMs = splashDurationMs,
@@ -37224,7 +37356,7 @@ Verification URL: `), bold5(fg7(PALETTE.humanAccent)(state.verificationUrl))] :
37224
37356
  fg7(PALETTE.bodyText)("Continue authentication in your browser."),
37225
37357
  ...url3,
37226
37358
  fg7(PALETTE.dimText)(`
37227
- ${spinner} Waiting for approval \xB7 q / esc / ctrl+c cancel`)
37359
+ ${spinner} Waiting for approval \xB7 q / esc cancel`)
37228
37360
  ]);
37229
37361
  }
37230
37362
  async function waitForSplashAbort(signal) {
@@ -37517,7 +37649,7 @@ ${approvalUrl}
37517
37649
  `);
37518
37650
  });
37519
37651
  if (!splashPresented)
37520
- dependencies.output.writeStderr(`Waiting for approval\u2026 Press Ctrl+C to cancel.
37652
+ dependencies.output.writeStderr(`Waiting for approval\u2026
37521
37653
  `);
37522
37654
  });
37523
37655
  let journalPrepared = false;
@@ -37681,7 +37813,6 @@ function parseOptionalStringFlag(value, name) {
37681
37813
  }
37682
37814
  async function createAuthenticatedClient(dependencies) {
37683
37815
  const paths = resolveCliPaths(dependencies.environment);
37684
- const candidateTokenPath = await createCandidateTokenPath(paths.tokenPath);
37685
37816
  const snapshot = await withCredentialState({
37686
37817
  stateDirectory: dirname6(paths.tokenPath),
37687
37818
  action: async () => {
@@ -37693,43 +37824,21 @@ async function createAuthenticatedClient(dependencies) {
37693
37824
  });
37694
37825
  const configuration = await readCliConfiguration(paths.configPath);
37695
37826
  const tokenGeneration = await readOptionalTokenFile(paths.tokenPath);
37696
- if (tokenGeneration !== undefined)
37697
- await writeFile4(candidateTokenPath, tokenGeneration, { mode: 384 });
37698
37827
  return { configuration, tokenGeneration };
37699
37828
  }
37700
37829
  });
37701
- const candidateAuthConfiguration = toAuthConfiguration({ configuration: snapshot.configuration, tokenPath: candidateTokenPath });
37702
- const identity = await loadStoredIdentity(candidateAuthConfiguration);
37830
+ const authConfiguration = toAuthConfiguration({ configuration: snapshot.configuration, tokenPath: paths.tokenPath });
37831
+ const identity = await loadStoredIdentity(authConfiguration);
37832
+ const getAccessToken = createRefreshingAccessTokenProvider({ dependencies, paths, snapshot });
37703
37833
  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
- });
37834
+ await getAccessToken(dependencies.abortSignal);
37726
37835
  const client = createCodingAgentClient({
37727
37836
  apiUrl: snapshot.configuration.apiUrl,
37728
- getAccessToken: async () => accessToken
37837
+ getAccessToken
37729
37838
  });
37730
37839
  const remoteIdentity = await materializeRemoteCurrentUser({ client, signal: dependencies.abortSignal });
37731
37840
  return {
37732
- authConfiguration: toAuthConfiguration({ configuration: snapshot.configuration, tokenPath: paths.tokenPath }),
37841
+ authConfiguration,
37733
37842
  client,
37734
37843
  identity,
37735
37844
  authenticatedEmail: remoteIdentity.email
@@ -37755,6 +37864,85 @@ async function createAuthenticatedClient(dependencies) {
37755
37864
  });
37756
37865
  }
37757
37866
  throw error93;
37867
+ }
37868
+ }
37869
+ function createRefreshingAccessTokenProvider({
37870
+ dependencies,
37871
+ paths,
37872
+ snapshot
37873
+ }) {
37874
+ let tokenGeneration = snapshot.tokenGeneration;
37875
+ let pendingAccessToken;
37876
+ return async (signal) => {
37877
+ if (!pendingAccessToken) {
37878
+ const expectedTokenGeneration = tokenGeneration;
37879
+ pendingAccessToken = refreshAccessToken({ dependencies, paths, configuration: snapshot.configuration, expectedTokenGeneration, signal }).then(({ accessToken: accessToken2, tokenGeneration: refreshedTokenGeneration }) => {
37880
+ tokenGeneration = refreshedTokenGeneration;
37881
+ return accessToken2;
37882
+ });
37883
+ }
37884
+ const accessToken = pendingAccessToken;
37885
+ try {
37886
+ return await accessToken;
37887
+ } finally {
37888
+ if (pendingAccessToken === accessToken)
37889
+ pendingAccessToken = undefined;
37890
+ }
37891
+ };
37892
+ }
37893
+ async function refreshAccessToken({
37894
+ dependencies,
37895
+ paths,
37896
+ configuration,
37897
+ expectedTokenGeneration,
37898
+ signal
37899
+ }) {
37900
+ const candidateTokenPath = await createCandidateTokenPath(paths.tokenPath);
37901
+ try {
37902
+ await withCredentialState({
37903
+ stateDirectory: dirname6(paths.tokenPath),
37904
+ action: async () => {
37905
+ await recoverCredentialPromotionWhileLocked({
37906
+ stateDirectory: dirname6(paths.tokenPath),
37907
+ configPath: paths.configPath,
37908
+ tokenPath: paths.tokenPath,
37909
+ ...dependencies.credentialStateFilesystem
37910
+ });
37911
+ await assertCredentialSnapshotCurrent({
37912
+ paths,
37913
+ snapshot: { configuration, tokenGeneration: expectedTokenGeneration }
37914
+ });
37915
+ if (expectedTokenGeneration !== undefined)
37916
+ await writeFile4(candidateTokenPath, expectedTokenGeneration, { mode: 384 });
37917
+ }
37918
+ });
37919
+ const candidateAuthConfiguration = toAuthConfiguration({ configuration, tokenPath: candidateTokenPath });
37920
+ const accessToken = await requireAccessToken(candidateAuthConfiguration, signal);
37921
+ const candidateTokenGeneration = await readFile5(candidateTokenPath, "utf8");
37922
+ await withCredentialState({
37923
+ stateDirectory: dirname6(paths.tokenPath),
37924
+ action: async () => {
37925
+ await recoverCredentialPromotionWhileLocked({
37926
+ stateDirectory: dirname6(paths.tokenPath),
37927
+ configPath: paths.configPath,
37928
+ tokenPath: paths.tokenPath,
37929
+ ...dependencies.credentialStateFilesystem
37930
+ });
37931
+ await assertCredentialSnapshotCurrent({
37932
+ paths,
37933
+ snapshot: { configuration, tokenGeneration: expectedTokenGeneration }
37934
+ });
37935
+ if (candidateTokenGeneration !== expectedTokenGeneration) {
37936
+ await replaceCredentialTokenWhileLocked({
37937
+ sourcePath: candidateTokenPath,
37938
+ tokenPath: paths.tokenPath,
37939
+ renameFile: dependencies.credentialStateFilesystem?.renameFile,
37940
+ syncDirectory: dependencies.credentialStateFilesystem?.syncDirectory
37941
+ });
37942
+ }
37943
+ }
37944
+ });
37945
+ return { accessToken, tokenGeneration: candidateTokenGeneration };
37758
37946
  } finally {
37759
37947
  await removeCandidateToken({ path: candidateTokenPath, removeTokenFile: unlink3 });
37760
37948
  }
@@ -37847,6 +38035,8 @@ async function dashboard({
37847
38035
  return 0;
37848
38036
  if (action.kind === "interrupt")
37849
38037
  throw new CliInterruptedError("SIGINT");
38038
+ if (action.kind === "logout")
38039
+ return await logout({ dependencies, flags: {} });
37850
38040
  holdTransitionScreen();
37851
38041
  if (action.kind === "session")
37852
38042
  return await attachSession({ dependencies, sessionId: action.sessionId, noTui: false, json: false });
@@ -38354,6 +38544,13 @@ async function runAttachedSession({
38354
38544
  bootstrap: await prepareDashboardBootstrap({ dependencies, operations })
38355
38545
  });
38356
38546
  }
38547
+ if (action.kind === "logout") {
38548
+ stop();
38549
+ await startPromise;
38550
+ if (dependencies.abortSignal?.aborted)
38551
+ throw dependencies.abortSignal.reason ?? new Error("interrupted");
38552
+ return await logout({ dependencies, flags: {} });
38553
+ }
38357
38554
  holdTransitionScreen();
38358
38555
  const availableRepositories = await collectAllRepositories({ listRepositories: operations.listRepositories });
38359
38556
  let draft = await openTuiNewSessionWizard({ dependencies, operations, repositories: availableRepositories });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/remy-cli",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Remy, the Coding Agent terminal client.",
5
5
  "type": "module",
6
6
  "bin": {