@meistrari/remy-cli 1.1.0 → 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 +315 -77
  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";
@@ -35110,6 +35198,8 @@ var defaultNewSessionAgent = {
35110
35198
  };
35111
35199
 
35112
35200
  // src/tui/new-session-wizard.ts
35201
+ var suggestionSpinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
35202
+ var suggestionSpinnerIntervalMs = 80;
35113
35203
  async function createNewSessionWizard({
35114
35204
  repositories,
35115
35205
  reloadRepositories,
@@ -35126,6 +35216,7 @@ async function createNewSessionWizard({
35126
35216
  let inputHandler;
35127
35217
  let keyHandler;
35128
35218
  let resizeHandler;
35219
+ let suggestionSpinnerTimer;
35129
35220
  try {
35130
35221
  let selectedInstallationId = function() {
35131
35222
  return installationIds[installationIndex];
@@ -35169,6 +35260,7 @@ async function createNewSessionWizard({
35169
35260
  }
35170
35261
  contentScrollBox.flexGrow = repositoryListVisible ? 0 : 1;
35171
35262
  contentScrollBox.maxHeight = repositoryListVisible ? "45%" : undefined;
35263
+ contentScrollBox.height = repositoryListVisible ? repositoryHeaderHeight ?? "auto" : 0;
35172
35264
  repositoryScrollBox.flexGrow = repositoryListVisible ? 1 : 0;
35173
35265
  repositoryScrollBox.visible = repositoryListVisible;
35174
35266
  repositoryRows.content = "";
@@ -35219,8 +35311,8 @@ async function createNewSessionWizard({
35219
35311
  } else if (step === "loadingSuggestions") {
35220
35312
  content.content = orient(joinStyled([
35221
35313
  new StyledText4([stepHeader("Select repositories")]),
35222
- stringToStyledText4("Finding repository suggestions\u2026"),
35223
- stringToStyledText4("This can take a few seconds."),
35314
+ new StyledText4([fg5(PALETTE.progress)(`${suggestionSpinnerFrames[suggestionSpinnerFrame]} Remy is matching your prompt to repositories\u2026`)]),
35315
+ new StyledText4([dim3(fg5(PALETTE.dimText)("This can take a few seconds."))]),
35224
35316
  stringToStyledText4("s select repositories yourself \xB7 esc back")
35225
35317
  ], `
35226
35318
 
@@ -35293,23 +35385,52 @@ async function createNewSessionWizard({
35293
35385
  if (composerMounted)
35294
35386
  composer.render();
35295
35387
  if (repositoryListVisible) {
35296
- renderer.once(CliRenderEvents2.FRAME, () => {
35388
+ renderer.once(CliRenderEvents3.FRAME, () => {
35297
35389
  if (destroyed || step !== "repositories" && step !== "repositorySearch")
35298
35390
  return;
35391
+ const nextRepositoryHeaderHeight = Math.min(content.virtualLineCount, Math.floor(renderer.height * 0.45));
35392
+ if (nextRepositoryHeaderHeight !== repositoryHeaderHeight) {
35393
+ repositoryHeaderHeight = nextRepositoryHeaderHeight;
35394
+ contentScrollBox.height = nextRepositoryHeaderHeight;
35395
+ }
35299
35396
  repositoryScrollBox.scrollTo({ x: 0, y: repositoryIndex });
35300
35397
  renderer.requestRender();
35301
35398
  });
35399
+ } else {
35400
+ repositoryHeaderHeight = undefined;
35302
35401
  }
35402
+ syncSuggestionLoadingIndicator();
35303
35403
  renderer.requestRender();
35404
+ }, syncSuggestionLoadingIndicator = function() {
35405
+ if (destroyed || settled || step !== "loadingSuggestions") {
35406
+ if (suggestionSpinnerTimer) {
35407
+ clearInterval(suggestionSpinnerTimer);
35408
+ suggestionSpinnerTimer = undefined;
35409
+ }
35410
+ suggestionSpinnerFrame = 0;
35411
+ return;
35412
+ }
35413
+ if (suggestionSpinnerTimer)
35414
+ return;
35415
+ suggestionSpinnerTimer = setInterval(() => {
35416
+ if (destroyed || settled || step !== "loadingSuggestions") {
35417
+ syncSuggestionLoadingIndicator();
35418
+ return;
35419
+ }
35420
+ suggestionSpinnerFrame = (suggestionSpinnerFrame + 1) % suggestionSpinnerFrames.length;
35421
+ render();
35422
+ }, suggestionSpinnerIntervalMs);
35304
35423
  }, finish = function(value) {
35305
35424
  if (settled)
35306
35425
  return;
35307
35426
  settled = true;
35427
+ syncSuggestionLoadingIndicator();
35308
35428
  resolveDraft(value);
35309
35429
  }, fail = function(error93) {
35310
35430
  if (settled)
35311
35431
  return;
35312
35432
  settled = true;
35433
+ syncSuggestionLoadingIndicator();
35313
35434
  rejectDraft(error93);
35314
35435
  }, returnToPromptFromRepositoryLoading = function() {
35315
35436
  step = "prompt";
@@ -35493,6 +35614,24 @@ async function createNewSessionWizard({
35493
35614
  if (step !== "repositories")
35494
35615
  return;
35495
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;
35496
35635
  if (key.name === "up" || key.name === "down" || key.name === "j" || key.name === "k") {
35497
35636
  key.preventDefault();
35498
35637
  if (choices.length > 0)
@@ -35560,10 +35699,13 @@ async function createNewSessionWizard({
35560
35699
  let repositoryIndex = 0;
35561
35700
  let repositoryQuery = "";
35562
35701
  let recomputeInstruction = "";
35702
+ let suggestionSpinnerFrame = 0;
35563
35703
  let attachments = [...initialDraft?.attachments ?? []];
35564
35704
  let pathCompletions = [];
35565
35705
  let completionIndex = 0;
35566
35706
  let pathCompletionMenuOpen = false;
35707
+ let repositoryHeaderHeight;
35708
+ let awaitingVimGoToTop = false;
35567
35709
  const manualSelections = new Map;
35568
35710
  let searchSelections;
35569
35711
  let suggestedIds = new Set;
@@ -35787,7 +35929,7 @@ async function createNewSessionWizard({
35787
35929
  };
35788
35930
  renderer.addInputHandler(inputHandler);
35789
35931
  renderer.keyInput.on("keypress", keyHandler);
35790
- renderer.on(CliRenderEvents2.RENDER_ERROR, (event) => fail(event.error));
35932
+ renderer.on(CliRenderEvents3.RENDER_ERROR, (event) => fail(event.error));
35791
35933
  resizeHandler = () => {
35792
35934
  if (destroyed)
35793
35935
  return;
@@ -35796,7 +35938,7 @@ async function createNewSessionWizard({
35796
35938
  repositoryIndex = Math.min(repositoryIndex, choices.length - 1);
35797
35939
  render();
35798
35940
  };
35799
- renderer.on(CliRenderEvents2.RESIZE, resizeHandler);
35941
+ renderer.on(CliRenderEvents3.RESIZE, resizeHandler);
35800
35942
  render();
35801
35943
  if (!Array.isArray(repositories))
35802
35944
  requestRepositoryLoad({ retry: false });
@@ -35811,7 +35953,11 @@ async function createNewSessionWizard({
35811
35953
  if (keyHandler)
35812
35954
  renderer.keyInput.off("keypress", keyHandler);
35813
35955
  if (resizeHandler)
35814
- renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
35956
+ renderer.off(CliRenderEvents3.RESIZE, resizeHandler);
35957
+ if (suggestionSpinnerTimer) {
35958
+ clearInterval(suggestionSpinnerTimer);
35959
+ suggestionSpinnerTimer = undefined;
35960
+ }
35815
35961
  composer.destroy();
35816
35962
  rendererDestroyed2 = true;
35817
35963
  renderer.destroy();
@@ -35821,12 +35967,14 @@ async function createNewSessionWizard({
35821
35967
  }
35822
35968
  };
35823
35969
  } catch (error93) {
35970
+ if (suggestionSpinnerTimer)
35971
+ clearInterval(suggestionSpinnerTimer);
35824
35972
  if (inputHandler)
35825
35973
  renderer.removeInputHandler(inputHandler);
35826
35974
  if (keyHandler)
35827
35975
  renderer.keyInput.off("keypress", keyHandler);
35828
35976
  if (resizeHandler)
35829
- renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
35977
+ renderer.off(CliRenderEvents3.RESIZE, resizeHandler);
35830
35978
  if (!rendererDestroyed2) {
35831
35979
  rendererDestroyed2 = true;
35832
35980
  renderer.destroy();
@@ -35909,11 +36057,12 @@ async function createDefaultRenderer2() {
35909
36057
  }
35910
36058
 
35911
36059
  // src/tui/session-view.ts
35912
- 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";
35913
36061
  var composerSlashCommands = [
35914
36062
  { value: "/sessions", description: "Back to the session list" },
35915
36063
  { value: "/complete", description: "Finish this session" },
35916
36064
  { value: "/new", description: "Start a new session" },
36065
+ { value: "/logout", description: "Sign out of Remy" },
35917
36066
  { value: "/help", description: "Show what you can do here" },
35918
36067
  { value: "/exit", description: "Leave Remy" }
35919
36068
  ];
@@ -36139,6 +36288,7 @@ async function createSessionTui({
36139
36288
  let stopState = { kind: "idle" };
36140
36289
  let completeState = { kind: "idle" };
36141
36290
  let helpVisible = false;
36291
+ let logoutConfirmationOpen = false;
36142
36292
  let latestState = controller.getState();
36143
36293
  let spinnerFrameIndex = 0;
36144
36294
  let renderedTranscript;
@@ -36195,6 +36345,17 @@ async function createSessionTui({
36195
36345
  return true;
36196
36346
  };
36197
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
+ }
36198
36359
  if (key.ctrl && key.name === "c") {
36199
36360
  key.preventDefault();
36200
36361
  finish({ kind: "exit", signal: "SIGINT" });
@@ -36270,6 +36431,10 @@ async function createSessionTui({
36270
36431
  }
36271
36432
  }
36272
36433
  composer.onKeyDown = (key) => {
36434
+ if (logoutConfirmationOpen) {
36435
+ key.preventDefault();
36436
+ return;
36437
+ }
36273
36438
  if (slashCompletions.length > 0 && (key.name === "up" || key.name === "down")) {
36274
36439
  key.preventDefault();
36275
36440
  slashCompletionIndex = (slashCompletionIndex + (key.name === "up" ? -1 : 1) + slashCompletions.length) % slashCompletions.length;
@@ -36423,6 +36588,14 @@ async function createSessionTui({
36423
36588
  await handleCompleteRequest();
36424
36589
  return true;
36425
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
+ }
36426
36599
  if (command === "/help") {
36427
36600
  composer.setText("");
36428
36601
  composerDraft = { ...composerDraft, text: "" };
@@ -36549,12 +36722,12 @@ async function createSessionTui({
36549
36722
  renderer.addInputHandler(inputHandler);
36550
36723
  renderer.keyInput.on("keypress", keyHandler);
36551
36724
  composer.setText(composerDraft.text);
36552
- renderer.on(CliRenderEvents3.RENDER_ERROR, (event) => finish(undefined, event.error));
36725
+ renderer.on(CliRenderEvents4.RENDER_ERROR, (event) => finish(undefined, event.error));
36553
36726
  resizeHandler = () => {
36554
36727
  if (!destroyed)
36555
36728
  render();
36556
36729
  };
36557
- renderer.on(CliRenderEvents3.RESIZE, resizeHandler);
36730
+ renderer.on(CliRenderEvents4.RESIZE, resizeHandler);
36558
36731
  render();
36559
36732
  if (latestState.aggregateStatus === "open")
36560
36733
  composer.focus();
@@ -36574,7 +36747,7 @@ async function createSessionTui({
36574
36747
  if (keyHandler)
36575
36748
  renderer.keyInput.off("keypress", keyHandler);
36576
36749
  if (resizeHandler)
36577
- renderer.off(CliRenderEvents3.RESIZE, resizeHandler);
36750
+ renderer.off(CliRenderEvents4.RESIZE, resizeHandler);
36578
36751
  composerLayout.destroy();
36579
36752
  rendererDestroyed2 = true;
36580
36753
  renderer.destroy();
@@ -36591,7 +36764,7 @@ async function createSessionTui({
36591
36764
  if (keyHandler)
36592
36765
  renderer.keyInput.off("keypress", keyHandler);
36593
36766
  if (resizeHandler)
36594
- renderer.off(CliRenderEvents3.RESIZE, resizeHandler);
36767
+ renderer.off(CliRenderEvents4.RESIZE, resizeHandler);
36595
36768
  if (!rendererDestroyed2) {
36596
36769
  rendererDestroyed2 = true;
36597
36770
  renderer.destroy();
@@ -36619,11 +36792,11 @@ var helpEntries = [
36619
36792
  { group: "command", token: "/new", description: "start a new session" },
36620
36793
  { group: "command", token: "/sessions", description: "back to the session list" },
36621
36794
  { group: "command", token: "/complete", description: "finish this session" },
36795
+ { group: "command", token: "/logout", description: "sign out of Remy" },
36622
36796
  { group: "command", token: "/exit", description: "leave Remy" },
36623
36797
  { group: "key", token: "esc", description: "stop Remy\u2019s current turn" },
36624
36798
  { group: "key", token: "ctrl+o", description: "expand the activity trail" },
36625
- { group: "key", token: "ctrl+r", description: "retry the oldest failed message" },
36626
- { group: "key", token: "ctrl+c", description: "quit" }
36799
+ { group: "key", token: "ctrl+r", description: "retry the oldest failed message" }
36627
36800
  ];
36628
36801
  function renderHelpPanel() {
36629
36802
  const width = helpEntries.reduce((max, entry) => Math.max(max, entry.token.length), 0) + 2;
@@ -36683,7 +36856,7 @@ function renderTerminalSessionBand({ aggregateStatus }) {
36683
36856
  }
36684
36857
  function renderActionBar({ state, working, stopState, completeState }) {
36685
36858
  if (completeState.kind === "completing")
36686
- 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"))]);
36687
36860
  if (state.aggregateStatus !== "open")
36688
36861
  return new StyledText5([]);
36689
36862
  const stopToken = working && stopState.kind === "failed" ? ["esc retry stop"] : [];
@@ -37001,7 +37174,7 @@ var compactMarkRows = 9;
37001
37174
  var compactMinWidth = 48;
37002
37175
  var compactMinHeight = 20;
37003
37176
  var markBrightnessGain = 4.2;
37004
- var remyCliVersion = "1.1.0";
37177
+ var remyCliVersion = "1.2.0";
37005
37178
  async function showRemySplash({
37006
37179
  createRenderer = createRemyRenderer,
37007
37180
  durationMs = splashDurationMs,
@@ -37183,7 +37356,7 @@ Verification URL: `), bold5(fg7(PALETTE.humanAccent)(state.verificationUrl))] :
37183
37356
  fg7(PALETTE.bodyText)("Continue authentication in your browser."),
37184
37357
  ...url3,
37185
37358
  fg7(PALETTE.dimText)(`
37186
- ${spinner} Waiting for approval \xB7 q / esc / ctrl+c cancel`)
37359
+ ${spinner} Waiting for approval \xB7 q / esc cancel`)
37187
37360
  ]);
37188
37361
  }
37189
37362
  async function waitForSplashAbort(signal) {
@@ -37476,7 +37649,7 @@ ${approvalUrl}
37476
37649
  `);
37477
37650
  });
37478
37651
  if (!splashPresented)
37479
- dependencies.output.writeStderr(`Waiting for approval\u2026 Press Ctrl+C to cancel.
37652
+ dependencies.output.writeStderr(`Waiting for approval\u2026
37480
37653
  `);
37481
37654
  });
37482
37655
  let journalPrepared = false;
@@ -37640,7 +37813,6 @@ function parseOptionalStringFlag(value, name) {
37640
37813
  }
37641
37814
  async function createAuthenticatedClient(dependencies) {
37642
37815
  const paths = resolveCliPaths(dependencies.environment);
37643
- const candidateTokenPath = await createCandidateTokenPath(paths.tokenPath);
37644
37816
  const snapshot = await withCredentialState({
37645
37817
  stateDirectory: dirname6(paths.tokenPath),
37646
37818
  action: async () => {
@@ -37652,43 +37824,21 @@ async function createAuthenticatedClient(dependencies) {
37652
37824
  });
37653
37825
  const configuration = await readCliConfiguration(paths.configPath);
37654
37826
  const tokenGeneration = await readOptionalTokenFile(paths.tokenPath);
37655
- if (tokenGeneration !== undefined)
37656
- await writeFile4(candidateTokenPath, tokenGeneration, { mode: 384 });
37657
37827
  return { configuration, tokenGeneration };
37658
37828
  }
37659
37829
  });
37660
- const candidateAuthConfiguration = toAuthConfiguration({ configuration: snapshot.configuration, tokenPath: candidateTokenPath });
37661
- 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 });
37662
37833
  try {
37663
- const accessToken = await requireAccessToken(candidateAuthConfiguration, dependencies.abortSignal);
37664
- await withCredentialState({
37665
- stateDirectory: dirname6(paths.tokenPath),
37666
- action: async () => {
37667
- await recoverCredentialPromotionWhileLocked({
37668
- stateDirectory: dirname6(paths.tokenPath),
37669
- configPath: paths.configPath,
37670
- tokenPath: paths.tokenPath,
37671
- ...dependencies.credentialStateFilesystem
37672
- });
37673
- await assertCredentialSnapshotCurrent({ paths, snapshot });
37674
- const candidateGeneration = await readFile5(candidateTokenPath, "utf8");
37675
- if (candidateGeneration !== snapshot.tokenGeneration) {
37676
- await replaceCredentialTokenWhileLocked({
37677
- sourcePath: candidateTokenPath,
37678
- tokenPath: paths.tokenPath,
37679
- renameFile: dependencies.credentialStateFilesystem?.renameFile,
37680
- syncDirectory: dependencies.credentialStateFilesystem?.syncDirectory
37681
- });
37682
- }
37683
- }
37684
- });
37834
+ await getAccessToken(dependencies.abortSignal);
37685
37835
  const client = createCodingAgentClient({
37686
37836
  apiUrl: snapshot.configuration.apiUrl,
37687
- getAccessToken: async () => accessToken
37837
+ getAccessToken
37688
37838
  });
37689
37839
  const remoteIdentity = await materializeRemoteCurrentUser({ client, signal: dependencies.abortSignal });
37690
37840
  return {
37691
- authConfiguration: toAuthConfiguration({ configuration: snapshot.configuration, tokenPath: paths.tokenPath }),
37841
+ authConfiguration,
37692
37842
  client,
37693
37843
  identity,
37694
37844
  authenticatedEmail: remoteIdentity.email
@@ -37714,6 +37864,85 @@ async function createAuthenticatedClient(dependencies) {
37714
37864
  });
37715
37865
  }
37716
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 };
37717
37946
  } finally {
37718
37947
  await removeCandidateToken({ path: candidateTokenPath, removeTokenFile: unlink3 });
37719
37948
  }
@@ -37806,6 +38035,8 @@ async function dashboard({
37806
38035
  return 0;
37807
38036
  if (action.kind === "interrupt")
37808
38037
  throw new CliInterruptedError("SIGINT");
38038
+ if (action.kind === "logout")
38039
+ return await logout({ dependencies, flags: {} });
37809
38040
  holdTransitionScreen();
37810
38041
  if (action.kind === "session")
37811
38042
  return await attachSession({ dependencies, sessionId: action.sessionId, noTui: false, json: false });
@@ -38313,6 +38544,13 @@ async function runAttachedSession({
38313
38544
  bootstrap: await prepareDashboardBootstrap({ dependencies, operations })
38314
38545
  });
38315
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
+ }
38316
38554
  holdTransitionScreen();
38317
38555
  const availableRepositories = await collectAllRepositories({ listRepositories: operations.listRepositories });
38318
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.0",
3
+ "version": "1.2.0",
4
4
  "description": "Remy, the Coding Agent terminal client.",
5
5
  "type": "module",
6
6
  "bin": {