@meistrari/remy-cli 1.25.1-rc.0 → 1.26.0-rc.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 +3 -3
  2. package/dist/remy.js +284 -115
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -70,7 +70,7 @@ The new-session request and conversation composers share path completion and att
70
70
 
71
71
  Terminal follow-ups default to **Steer**, targeting the work currently in progress. Press `Ctrl+Enter` to queue a message as **Next**, which waits for current work to finish. Plain `Enter` continues to send as Steer. Failed submissions retain their mode, attachments, settings, and idempotency key when retried with `Ctrl+R`. Remy can replay sessions containing queued-message dispatch, withdrawal, and failure events. Messages withdrawn or failed before reaching Remy show an activity notice; an active submission ends as cancelled or failed instead of waiting for a turn that never started.
72
72
 
73
- Use Up/Down to select a session in the dashboard, Left/Right to load the adjacent page, and `Enter` to open it. Remy loads the complete retained conversation before showing the session view, then streams new activity as it arrives. Press `s` to search the loaded page by session ID, title, or number; `Enter` keeps the search active and `Esc` clears it. Press `f` to filter sessions by lifecycle status or `a` to filter by author. Both pickers use checkboxes: Up/Down or `j`/`k` moves, `Space` toggles, Right/`l` checks, Left/`h` clears, `G` and `gg` jump to the last and first option, and `Enter` applies every checked value. An empty picker means all values. Press `s` inside either picker to fuzzy-search its options; `Enter` keeps the matching list and `Esc` cancels the search. Press `Esc` outside search to discard staged checkbox changes. Filters apply to the complete remote result set and reset pagination to its first page. 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 any message composer, plain `Enter` and `Shift+Enter` submit; `Option+Enter` on macOS (`Alt+Enter` elsewhere) inserts a newline. `Ctrl+J` also inserts a newline for terminals that do not report the Option/Alt modifier. `Cmd+Left`/`Cmd+Right` move to the start or end of the current line, as do `Home`/`End`, which many macOS terminals send for those keys; `Ctrl+D` deletes the character after the cursor. In the session view, `Ctrl+C` first clears a non-empty draft; press it again with an empty composer to exit. The conversation header keeps the numbered session title on its first line and repository, pull request, work state, elapsed time, connection state, and the agent's current `Harness · Model · Effort` on its second line. The header also keeps the last provider-reported context observation across reconnects, such as `Context 50% left · 62,000 used`; when the provider does not report a context window, it shows only used tokens. This is an observation, not a live occupancy guarantee or billing total.
73
+ Use Up/Down to select a session in the dashboard, Left/Right to load the adjacent page, and `Enter` to open it. Remy shows the session header and composer immediately after session details load, while retained conversation history loads in the background. You can start drafting immediately; sending is paused until history finishes, and your draft stays intact. New activity then streams as it arrives. Press `s` to fuzzy-search all accessible sessions through the API by author, repository, branch name, pull request title/number, or session number/ID/title; `Enter` applies immediately and keeps the search active; `Esc` clears it. Typing refreshes results after a short pause. Matching ignores case and query whitespace and finds characters in order within a field (for example, `grch` matches `Grace Hopper`); numbers also accept `#`. Search combines with your other filters and stays active across pages. Session rows and compact cards show the repository and session work branch (`Branch pending` when unassigned). Press `f` to filter sessions by lifecycle status, `a` by author, or `r` by repository. These pickers use checkboxes: Up/Down or `j`/`k` moves, `Space` toggles, Right/`l` checks, Left/`h` clears, `G` and `gg` jump to the last and first option, and `Enter` applies every checked value. An empty picker means all values. Press `s` inside a picker to fuzzy-search its options; `Enter` keeps the matching list and `Esc` cancels the search. Press `Esc` outside search to discard staged checkbox changes. Press `b` to filter by a case-insensitive part of a branch name; Enter applies it, an empty value clears it, and Esc discards edits. Repository and branch filters must match the same repository in a session. Filters apply to the complete remote result set and reset pagination to its first page. 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 any message composer, plain `Enter` and `Shift+Enter` submit; `Option+Enter` on macOS (`Alt+Enter` elsewhere) inserts a newline. `Ctrl+J` also inserts a newline for terminals that do not report the Option/Alt modifier. `Cmd+Left`/`Cmd+Right` move to the start or end of the current line, as do `Home`/`End`, which many macOS terminals send for those keys; `Ctrl+D` deletes the character after the cursor. In the session view, `Ctrl+C` first clears a non-empty draft; press it again with an empty composer to exit. The conversation header keeps the numbered session title on its first line and repository, pull request, work state, elapsed time, connection state, and the agent's current `Harness · Model · Effort` on its second line. The header also keeps the last provider-reported context observation across reconnects, such as `Context 50% left · 62,000 used`; when the provider does not report a context window, it shows only used tokens. This is an observation, not a live occupancy guarantee or billing total.
74
74
 
75
75
  If a connected session loses its activity stream, Remy retries for up to two minutes and resumes from the last received event. After the live stream stays connected for at least 30 seconds, the next disconnect starts a fresh retry window. Connections that drop sooner share the existing window and backoff. While a submitted message is still awaiting its outcome, retries continue beyond that window.
76
76
 
@@ -100,7 +100,7 @@ Command and attachment feedback share progress, success, failure, and attention
100
100
 
101
101
  Enter `/compact` to admit a durable main-thread context compaction control without sending a human message. Remy reports request admission immediately and shows the eventual completed, no-op, failed, or unknown provider result in retained activity. Compaction waits for a safe point and does not interrupt active work or jump ahead of an earlier Next message. While admission is pending, you can keep editing or open other commands; repeated `/compact` invocations do not send another request.
102
102
 
103
- 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. Published file artifacts show their filename and muted, full web preview URL; open the URL in a browser to select that artifact in its session. Tela Page rows retain their canonical Page URL. Downloads are explicit through `/files`; artifact bytes are never printed in the terminal. When the conversation has focus, press `Tab` to return to the composer.
103
+ 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. Published file artifacts show their filename and muted, full web preview URL; open the URL in a browser to select that artifact in its session. Tela Page rows retain their canonical Page URL. Downloads are explicit through `/files`; artifact bytes are never printed in the terminal. Clicking the conversation keeps keyboard focus in the composer. Mouse scrolling and drag-to-select still work. Code blocks and expanded code/output use colored text on a distinct background without copyable gutter bars or added line indentation.
104
104
 
105
105
  | Control | Result |
106
106
  | --- | --- |
@@ -131,7 +131,7 @@ After a session becomes terminal, press `Esc` to return to the dashboard or `n`
131
131
 
132
132
  `Shift+L` in the dashboard and `/logout` in a session ask for confirmation; press `y` to sign out or `Esc` to cancel. In the dashboard and repository-review list, Vim navigation is also available: `j`/`k` move, `h`/`l` change dashboard pages (or clear/mark a repository), `G` goes to the last row, and `gg` goes to the first row.
133
133
 
134
- 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.
134
+ 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. Clicking the conversation keeps keyboard focus in the composer. Mouse scrolling and drag-to-select still work. Code blocks and expanded code/output use colored text on a distinct background without copyable gutter bars or added line indentation.
135
135
 
136
136
  </details>
137
137
 
package/dist/remy.js CHANGED
@@ -33544,7 +33544,8 @@ var sessionDetailResponseSchema = exports_external2.object({
33544
33544
  }).passthrough()),
33545
33545
  repositories: exports_external2.array(exports_external2.strictObject({
33546
33546
  id: exports_external2.string(),
33547
- full_name: exports_external2.string()
33547
+ full_name: exports_external2.string(),
33548
+ branch_name: exports_external2.string().nullable().optional()
33548
33549
  })),
33549
33550
  connections: exports_external2.array(sessionConnectionSchema).max(1),
33550
33551
  artifacts: exports_external2.array(sessionArtifactSchema),
@@ -33594,7 +33595,8 @@ var sessionSummarySchema = exports_external2.object({
33594
33595
  source: sessionSourceSchema,
33595
33596
  repositories: exports_external2.array(exports_external2.strictObject({
33596
33597
  id: exports_external2.string(),
33597
- full_name: exports_external2.string()
33598
+ full_name: exports_external2.string(),
33599
+ branch_name: exports_external2.string().nullable().default(null)
33598
33600
  })),
33599
33601
  created_by: sessionCreatorSchema,
33600
33602
  created_at: exports_external2.string(),
@@ -33747,6 +33749,9 @@ async function listSessions({
33747
33749
  after,
33748
33750
  statuses,
33749
33751
  creatorUserIds,
33752
+ repositoryIds,
33753
+ branchName,
33754
+ search,
33750
33755
  signal
33751
33756
  }) {
33752
33757
  const searchParams = new URLSearchParams;
@@ -33758,6 +33763,12 @@ async function listSessions({
33758
33763
  searchParams.append("status", status);
33759
33764
  for (const creatorUserId of creatorUserIds ?? [])
33760
33765
  searchParams.append("creator_user_id", creatorUserId);
33766
+ for (const repositoryId of repositoryIds ?? [])
33767
+ searchParams.append("repository_id", repositoryId);
33768
+ if (branchName)
33769
+ searchParams.set("branch_name", branchName);
33770
+ if (search)
33771
+ searchParams.set("search", search);
33761
33772
  const suffix = searchParams.size > 0 ? `?${searchParams.toString()}` : "";
33762
33773
  const response = await client.request(`/v1/sessions${suffix}`, { signal });
33763
33774
  const parsed = sessionListResponseSchema.safeParse(await parseJson2(response, "Session list response was not valid JSON."));
@@ -33777,7 +33788,7 @@ async function listSessions({
33777
33788
  draft: pullRequest.draft
33778
33789
  })),
33779
33790
  source: sessionSourceLabel(session.source),
33780
- repositories: session.repositories.map((repository) => ({ id: repository.id, fullName: repository.full_name })),
33791
+ repositories: session.repositories.map((repository) => ({ id: repository.id, fullName: repository.full_name, branchName: repository.branch_name })),
33781
33792
  creator: sessionCreatorLabel(session.created_by),
33782
33793
  createdAt: session.created_at,
33783
33794
  updatedAt: session.updated_at
@@ -34592,7 +34603,7 @@ function formatSessionJson({
34592
34603
  `;
34593
34604
  }
34594
34605
  // src/version.ts
34595
- var remyCliVersion = "1.25.1-rc.0";
34606
+ var remyCliVersion = "1.26.0-rc.0";
34596
34607
 
34597
34608
  // src/self-update.ts
34598
34609
  import { spawn } from "child_process";
@@ -35092,6 +35103,7 @@ function createSessionViewState({ detail, activeMessageId }) {
35092
35103
  title: detail.title ?? null,
35093
35104
  aggregateStatus: detail.status,
35094
35105
  ...detail.agentStatus === undefined ? {} : { agentStatus: detail.agentStatus },
35106
+ historyStatus: "ready",
35095
35107
  connectionStatus: "connected",
35096
35108
  pullRequest: toSessionPullRequest(detail),
35097
35109
  agent: toSessionAgentSettings(detail.agent),
@@ -36411,19 +36423,22 @@ function createRemoteSessionController(dependencies) {
36411
36423
  ready = new Promise((resolve) => {
36412
36424
  resolveReady = resolve;
36413
36425
  });
36426
+ state = { ...createSessionViewState({ detail: input.detail, activeMessageId: dependencies.activeMessageId }), historyStatus: "loading" };
36414
36427
  const cache = await readSessionCache(cachePath).catch((error93) => {
36415
36428
  handleCacheError(error93);
36416
36429
  return null;
36417
36430
  });
36418
36431
  cachedLastRetainedEventId = input.mode === "live" ? input.lastRetainedEventId ?? cache?.lastRetainedEventId : undefined;
36419
- state = createSessionViewState({
36420
- detail: input.detail,
36421
- activeMessageId: dependencies.activeMessageId ?? cache?.activeMessageId
36422
- });
36432
+ if (stopped)
36433
+ return;
36434
+ state = { ...getState(), activeMessageId: dependencies.activeMessageId ?? cache?.activeMessageId };
36423
36435
  if (input.mode === "cold-resume") {
36424
36436
  const hydrationGeneration = beginReasoningAttempt();
36425
36437
  await hydrateRetainedHistoryFromBeginning(hydrationGeneration);
36426
36438
  }
36439
+ if (stopped)
36440
+ return;
36441
+ state = { ...getState(), historyStatus: "ready" };
36427
36442
  resolveReady();
36428
36443
  publishState();
36429
36444
  try {
@@ -37805,10 +37820,10 @@ import { BoxRenderable as BoxRenderable2, TextRenderable, TextareaRenderable } f
37805
37820
  import { dim as dim2, fg as fg3, StyledText as StyledText4 } from "@opentui/core";
37806
37821
 
37807
37822
  // src/tui/theme.ts
37808
- import { bold, dim, fg as fg2, stringToStyledText, StyledText as StyledText3 } from "@opentui/core";
37823
+ import { bg as bg2, bold, dim, fg as fg2, stringToStyledText, StyledText as StyledText3 } from "@opentui/core";
37809
37824
 
37810
37825
  // src/tui/markdown.ts
37811
- import { fg, StyledText as StyledText2, TextAttributes as TextAttributes2 } from "@opentui/core";
37826
+ import { bg, fg, StyledText as StyledText2, TextAttributes as TextAttributes2 } from "@opentui/core";
37812
37827
 
37813
37828
  // ../../node_modules/.bun/marked@17.0.1/node_modules/marked/lib/marked.esm.js
37814
37829
  function L() {
@@ -39275,7 +39290,7 @@ function renderCodeBlock(token, palette) {
39275
39290
  `)) {
39276
39291
  chunks.push(plain(`
39277
39292
  `, { color: palette.body, attributes: 0 }));
39278
- chunks.push(plain("\u2502 ", { color: palette.codeBar, attributes: 0 }), plain(line, { color: palette.code, attributes: 0 }));
39293
+ chunks.push(plain(line, { color: palette.code, background: palette.codeBackground, attributes: 0 }));
39279
39294
  }
39280
39295
  return chunks;
39281
39296
  }
@@ -39368,6 +39383,8 @@ function trimTrailingNewlines(chunks) {
39368
39383
  }
39369
39384
  function plain(text, style) {
39370
39385
  const chunk = fg(style.color)(text);
39386
+ if (style.background)
39387
+ chunk.bg = bg(style.background)(text).bg;
39371
39388
  if (style.attributes)
39372
39389
  chunk.attributes = style.attributes;
39373
39390
  if (style.link)
@@ -39385,6 +39402,7 @@ var PALETTE = {
39385
39402
  codeLabel: "#64748B",
39386
39403
  codeText: "#93C5FD",
39387
39404
  codeBar: "#475569",
39405
+ codeBackground: "#20252E",
39388
39406
  reasoning: "#C084FC",
39389
39407
  progress: "#60A5FA",
39390
39408
  tool: "#22D3EE",
@@ -39540,7 +39558,7 @@ function renderFencedCodeBody(text) {
39540
39558
  `)) {
39541
39559
  chunks.push(...stringToStyledText(`
39542
39560
  `).chunks);
39543
- chunks.push(fg2(PALETTE.codeBar)("\u2502 "), fg2(PALETTE.codeText)(line));
39561
+ chunks.push(bg2(PALETTE.codeBackground)(fg2(PALETTE.codeText)(line)));
39544
39562
  }
39545
39563
  cursor = start + fence.length;
39546
39564
  }
@@ -39560,7 +39578,8 @@ function renderFormattedMessageBody(text, role2) {
39560
39578
  muted: PALETTE.dimText,
39561
39579
  code: PALETTE.codeText,
39562
39580
  codeLabel: PALETTE.codeLabel,
39563
- codeBar: PALETTE.codeBar
39581
+ codeBar: PALETTE.codeBar,
39582
+ codeBackground: PALETTE.codeBackground
39564
39583
  });
39565
39584
  if (formattedMessages.size >= FORMATTED_MESSAGE_CACHE_LIMIT)
39566
39585
  formattedMessages.delete(formattedMessages.keys().next().value);
@@ -39568,14 +39587,12 @@ function renderFormattedMessageBody(text, role2) {
39568
39587
  return formatted;
39569
39588
  }
39570
39589
  function renderCodeSnippet({ text, language = "code", indent = "" }) {
39571
- const gutter = `${indent}\u2502 `;
39572
39590
  return new StyledText3([
39573
39591
  ...indent ? [dim(fg2(PALETTE.dimText)(indent))] : [],
39574
39592
  dim(fg2(PALETTE.codeLabel)(language)),
39575
39593
  fg2(PALETTE.codeText)(`
39576
- ${gutter}${text.replaceAll(`
39577
- `, `
39578
- ${gutter}`)}`)
39594
+ `),
39595
+ bg2(PALETTE.codeBackground)(fg2(PALETTE.codeText)(text))
39579
39596
  ]);
39580
39597
  }
39581
39598
  function formatTime(occurredAt) {
@@ -39761,12 +39778,12 @@ function renderKeyHints({ hints, width }) {
39761
39778
  }
39762
39779
 
39763
39780
  // src/tui/selection.ts
39764
- import { bg, bold as bold2, fg as fg5 } from "@opentui/core";
39781
+ import { bg as bg3, bold as bold2, fg as fg5 } from "@opentui/core";
39765
39782
  function selectionLabel({ text, checked }) {
39766
39783
  return checked ? bold2(fg5(PALETTE.humanAccent)(text)) : fg5(PALETTE.dimText)(text);
39767
39784
  }
39768
39785
  function highlightSelection({ chunks, cursor }) {
39769
- return cursor ? chunks.map((chunk) => bg(PALETTE.selectionBg)(chunk)) : chunks;
39786
+ return cursor ? chunks.map((chunk) => bg3(PALETTE.selectionBg)(chunk)) : chunks;
39770
39787
  }
39771
39788
 
39772
39789
  // src/tui/renderer.ts
@@ -40659,7 +40676,9 @@ var lifecycleStatusOptions = [
40659
40676
  ];
40660
40677
  async function createDashboardTui({
40661
40678
  initialPage,
40662
- initialFilters = { statuses: [], creators: [] },
40679
+ initialFilters = { statuses: [], creators: [], repositories: [], branchName: "" },
40680
+ repositories: initialRepositories = [],
40681
+ loadRepositories,
40663
40682
  authors: initialAuthors = [],
40664
40683
  loadAuthors,
40665
40684
  loadPage,
@@ -40670,23 +40689,27 @@ async function createDashboardTui({
40670
40689
  let keyHandler;
40671
40690
  let resizeHandler;
40672
40691
  let refreshTimer;
40692
+ let searchTimer;
40673
40693
  let contentScrollHandler;
40674
40694
  let rendererDestroyed2 = false;
40675
40695
  const loadPageAbortController = new AbortController;
40676
40696
  try {
40677
40697
  let stopPageRefresh = function() {
40698
+ clearTimeout(searchTimer);
40678
40699
  if (refreshTimer !== undefined) {
40679
40700
  clearInterval(refreshTimer);
40680
40701
  refreshTimer = undefined;
40681
40702
  }
40682
40703
  loadPageAbortController.abort();
40704
+ }, currentOptionState = function() {
40705
+ return filterMode?.kind === "creator" || filterMode?.kind === "repository" ? optionStates[filterMode.kind] : undefined;
40683
40706
  }, render = function() {
40684
40707
  const contentWidth = Math.max(1, renderer.width - TUI_LAYOUT.dashboardGutter * 2);
40685
- const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
40708
+ const sessions = page.sessions.map((session) => prioritizeSessionMatches(session, filters, sessionSearchQuery));
40686
40709
  const rowsBelow = rowsBelowVisibleWindow({ sessionCount: sessions.length, selectedIndex, height: renderer.height });
40687
40710
  const isNarrow = renderer.width < TUI_LAYOUT.dashboardCardBreakpoint;
40688
40711
  const selectedSession = filterMode ? undefined : sessions[selectedIndex];
40689
- const sessionRows = filterMode ? formatFilterMenu({ mode: filterMode, authors, width: contentWidth }) : sessions.length === 0 ? stringToStyledText2(sessionSearchQuery ? "No sessions match the search." : hasActiveFilters(filters) ? "No sessions match the active filters." : "No remote sessions yet.") : isNarrow ? formatSessionCard({
40712
+ const sessionRows = filterMode ? formatFilterMenu({ mode: filterMode, authors, repositories, width: contentWidth }) : sessions.length === 0 ? stringToStyledText2(sessionSearchQuery ? "No sessions match the search." : hasActiveFilters(filters) ? "No sessions match the active filters." : "No remote sessions yet.") : isNarrow ? formatSessionCard({
40690
40713
  session: selectedSession,
40691
40714
  selectedIndex,
40692
40715
  sessionCount: sessions.length,
@@ -40736,7 +40759,7 @@ async function createDashboardTui({
40736
40759
  else if (activeSearchComposer === "session")
40737
40760
  sessionSearchComposer.render();
40738
40761
  separator.content = activeSearchComposer ? "" : renderComposerDivider({ width: contentWidth });
40739
- footer.content = filterMode ? dashboardFilterFooter({ mode: filterMode, loadingPage, loadingAuthors, pageError, authorError, width: contentWidth }) : dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen, sessionSearchOpen, width: contentWidth });
40762
+ footer.content = filterMode ? dashboardFilterFooter({ mode: filterMode, loadingPage, loadingOptions: currentOptionState()?.loading ?? false, pageError, optionError: currentOptionState()?.error, width: contentWidth }) : dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen, sessionSearchOpen, width: contentWidth });
40740
40763
  renderer.requestRender();
40741
40764
  }, finish = function(value) {
40742
40765
  if (settled)
@@ -40751,7 +40774,7 @@ async function createDashboardTui({
40751
40774
  stopPageRefresh();
40752
40775
  rejectDraft(error93);
40753
40776
  }, moveSelectionTo = function(index) {
40754
- const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
40777
+ const sessions = page.sessions;
40755
40778
  if (sessions.length === 0)
40756
40779
  return;
40757
40780
  selectedIndex = Math.max(0, Math.min(index, sessions.length - 1));
@@ -40759,6 +40782,10 @@ async function createDashboardTui({
40759
40782
  }, applyFilterSearch = function() {
40760
40783
  if (!filterMode?.searching)
40761
40784
  return;
40785
+ if (filterMode.kind === "branch") {
40786
+ applyFilters({ ...filters, statuses: filterMode.statuses, creators: filterMode.creators, repositories: filterMode.repositories, branchName: filterMode.query.trim() });
40787
+ return;
40788
+ }
40762
40789
  filterMode = { ...filterMode, searching: false, selectedIndex: 0 };
40763
40790
  filterSearchComposer.textarea.setText("");
40764
40791
  render();
@@ -40777,6 +40804,7 @@ async function createDashboardTui({
40777
40804
  return;
40778
40805
  sessionSearchOpen = false;
40779
40806
  selectedIndex = 0;
40807
+ loadSessionSearch();
40780
40808
  render();
40781
40809
  }, cancelSessionSearch = function() {
40782
40810
  if (!sessionSearchOpen)
@@ -40785,6 +40813,7 @@ async function createDashboardTui({
40785
40813
  sessionSearchQuery = "";
40786
40814
  selectedIndex = 0;
40787
40815
  sessionSearchComposer.textarea.setText("");
40816
+ loadSessionSearch();
40788
40817
  render();
40789
40818
  }, handleSessionKey = function(key) {
40790
40819
  if (logoutConfirmationOpen) {
@@ -40805,22 +40834,21 @@ async function createDashboardTui({
40805
40834
  return;
40806
40835
  }
40807
40836
  if (filterMode) {
40808
- const options = visibleDashboardFilterOptions({ mode: filterMode, authors });
40837
+ const options = visibleDashboardFilterOptions({ mode: filterMode, authors, repositories });
40809
40838
  if (key.name === "escape") {
40810
40839
  key.preventDefault();
40811
- if (filterMode.searching) {
40840
+ if (filterMode.searching && filterMode.kind !== "branch") {
40812
40841
  cancelFilterSearch();
40813
40842
  return;
40814
40843
  }
40815
40844
  filterMode = undefined;
40816
40845
  pageError = undefined;
40817
- authorError = undefined;
40818
40846
  render();
40819
40847
  return;
40820
40848
  }
40821
40849
  if (filterMode.searching)
40822
40850
  return;
40823
- if (loadingAuthors) {
40851
+ if (currentOptionState()?.loading) {
40824
40852
  key.preventDefault();
40825
40853
  return;
40826
40854
  }
@@ -40857,7 +40885,7 @@ async function createDashboardTui({
40857
40885
  if (!option)
40858
40886
  return;
40859
40887
  const selected = key.name === "space" ? !isDashboardFilterOptionSelected({ mode: filterMode, option }) : key.name === "right" || key.name === "l";
40860
- filterMode = updateDashboardFilterSelection({ mode: filterMode, option, selected, authors });
40888
+ filterMode = updateDashboardFilterSelection({ mode: filterMode, option, selected, authors, repositories });
40861
40889
  render();
40862
40890
  return;
40863
40891
  }
@@ -40870,7 +40898,7 @@ async function createDashboardTui({
40870
40898
  }
40871
40899
  if (key.name === "return" || key.name === "enter") {
40872
40900
  key.preventDefault();
40873
- const nextFilters = { statuses: [...filterMode.statuses], creators: [...filterMode.creators] };
40901
+ const nextFilters = { ...filters, statuses: [...filterMode.statuses], creators: [...filterMode.creators], repositories: [...filterMode.repositories], branchName: filterMode.branchName };
40874
40902
  applyFilters(nextFilters);
40875
40903
  }
40876
40904
  return;
@@ -40903,7 +40931,7 @@ async function createDashboardTui({
40903
40931
  if (listFocused && (key.name === "G" || key.name === "g" && key.shift)) {
40904
40932
  key.preventDefault();
40905
40933
  awaitingVimGoToTop = false;
40906
- moveSelectionTo(visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery }).length - 1);
40934
+ moveSelectionTo(page.sessions.length - 1);
40907
40935
  return;
40908
40936
  }
40909
40937
  if (listFocused && key.name === "g") {
@@ -40928,7 +40956,9 @@ async function createDashboardTui({
40928
40956
  awaitingVimGoToTop = false;
40929
40957
  if (key.name === "return" || key.name === "enter") {
40930
40958
  key.preventDefault();
40931
- const session = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery })[selectedIndex];
40959
+ if (loadingPage || sessionSearchQuery.trim() !== (filters.search ?? ""))
40960
+ return;
40961
+ const session = page.sessions[selectedIndex];
40932
40962
  if (session)
40933
40963
  finish({ kind: "session", sessionId: session.id });
40934
40964
  return;
@@ -40943,13 +40973,27 @@ async function createDashboardTui({
40943
40973
  openSessionSearch();
40944
40974
  return;
40945
40975
  }
40976
+ if (["f", "a", "r", "b"].includes(key.name) && (loadingPage || sessionSearchQuery.trim() !== (filters.search ?? "")))
40977
+ return;
40946
40978
  if (key.name === "f") {
40947
40979
  key.preventDefault();
40948
- authorError = undefined;
40949
40980
  filterMode = createFilterMode({ kind: "status", filters });
40950
40981
  render();
40951
40982
  return;
40952
40983
  }
40984
+ if (key.name === "r" || key.name === "b") {
40985
+ key.preventDefault();
40986
+ filterMode = createFilterMode({ kind: key.name === "r" ? "repository" : "branch", filters });
40987
+ if (filterMode.kind === "branch") {
40988
+ filterMode.searching = true;
40989
+ filterMode.query = filters.branchName;
40990
+ filterSearchComposer.textarea.setText(filters.branchName);
40991
+ }
40992
+ render();
40993
+ if (key.name === "r")
40994
+ loadRepositoryOptions();
40995
+ return;
40996
+ }
40953
40997
  if (key.name === "a") {
40954
40998
  key.preventDefault();
40955
40999
  openAuthorFilter();
@@ -40992,17 +41036,22 @@ async function createDashboardTui({
40992
41036
  let selectedIndex = 0;
40993
41037
  let loadingPage = false;
40994
41038
  let pageRequestInFlight = false;
41039
+ let pageRequestVersion = 0;
40995
41040
  let pageError;
40996
- let authorError;
41041
+ const optionStates = {
41042
+ creator: { loading: false, error: undefined },
41043
+ repository: { loading: false, error: undefined }
41044
+ };
41045
+ let repositories = initialRepositories;
41046
+ let repositoriesLoaded = loadRepositories === undefined || initialRepositories.length > 0;
40997
41047
  let authors = initialAuthors;
40998
41048
  let authorsLoaded = loadAuthors === undefined || initialAuthors.length > 0;
40999
- let loadingAuthors = false;
41000
41049
  let awaitingVimGoToTop = false;
41001
41050
  let logoutConfirmationOpen = false;
41002
41051
  let filters = initialFilters;
41003
41052
  let filterMode;
41004
41053
  let sessionSearchOpen = false;
41005
- let sessionSearchQuery = "";
41054
+ let sessionSearchQuery = initialFilters.search ?? "";
41006
41055
  root.onMouseDown = (event) => {
41007
41056
  if (filterMode?.searching || sessionSearchOpen)
41008
41057
  return;
@@ -41042,7 +41091,7 @@ async function createDashboardTui({
41042
41091
  renderer,
41043
41092
  parent: filterSearchRegion,
41044
41093
  tag: "Search",
41045
- placeholder: "Search by ID, title, or number\u2026",
41094
+ placeholder: "Search sessions, authors, repositories, branches, or PRs\u2026",
41046
41095
  screenPaddingX: 2,
41047
41096
  onSubmit: () => applySessionSearch(),
41048
41097
  onContentChange: () => {
@@ -41050,104 +41099,151 @@ async function createDashboardTui({
41050
41099
  return;
41051
41100
  sessionSearchQuery = sessionSearchComposer.textarea.plainText;
41052
41101
  selectedIndex = 0;
41102
+ pageRequestVersion += 1;
41103
+ loadingPage = true;
41104
+ clearTimeout(searchTimer);
41105
+ searchTimer = setTimeout(() => {
41106
+ searchTimer = undefined;
41107
+ loadSessionSearch();
41108
+ }, 200);
41053
41109
  render();
41054
41110
  }
41055
41111
  });
41056
41112
  async function loadAdjacentPage(direction) {
41057
- if (pageRequestInFlight)
41113
+ if (pageRequestInFlight || loadingPage || sessionSearchQuery.trim() !== (filters.search ?? ""))
41058
41114
  return;
41059
41115
  const canLoad = direction === "next" ? page.hasMore : page.canGoPrevious;
41060
41116
  if (!canLoad)
41061
41117
  return;
41118
+ const requestVersion = ++pageRequestVersion;
41062
41119
  pageRequestInFlight = true;
41063
41120
  loadingPage = true;
41064
41121
  pageError = undefined;
41065
41122
  render();
41066
41123
  try {
41067
41124
  const loadedPage = await loadPage({ target: direction, signal: loadPageAbortController.signal });
41068
- if (destroyed || settled)
41125
+ if (destroyed || settled || requestVersion !== pageRequestVersion)
41069
41126
  return;
41070
41127
  page = loadedPage;
41071
41128
  pageIndex = Math.max(1, pageIndex + (direction === "next" ? 1 : -1));
41072
- const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
41129
+ const sessions = page.sessions;
41073
41130
  selectedIndex = direction === "next" ? 0 : Math.max(0, sessions.length - 1);
41074
41131
  } catch (error93) {
41075
- if (!destroyed && !settled)
41132
+ if (!destroyed && !settled && requestVersion === pageRequestVersion)
41076
41133
  pageError = error93 instanceof Error ? error93.message : String(error93);
41077
41134
  } finally {
41078
- pageRequestInFlight = false;
41079
- loadingPage = false;
41080
- if (!destroyed && !settled)
41081
- render();
41135
+ if (requestVersion === pageRequestVersion) {
41136
+ pageRequestInFlight = false;
41137
+ loadingPage = false;
41138
+ if (!destroyed && !settled)
41139
+ render();
41140
+ }
41082
41141
  }
41083
41142
  }
41084
41143
  async function refreshCurrentPage() {
41085
- if (pageRequestInFlight || destroyed || settled)
41144
+ if (pageRequestInFlight || loadingPage || destroyed || settled || sessionSearchQuery.trim() !== (filters.search ?? ""))
41086
41145
  return;
41146
+ const requestVersion = ++pageRequestVersion;
41087
41147
  pageRequestInFlight = true;
41088
41148
  try {
41089
41149
  const refreshedPage = await loadPage({ target: "current", signal: loadPageAbortController.signal });
41090
- if (destroyed || settled)
41150
+ if (destroyed || settled || requestVersion !== pageRequestVersion)
41091
41151
  return;
41092
- const selectedSessionId = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery })[selectedIndex]?.id;
41152
+ const selectedSessionId = page.sessions[selectedIndex]?.id;
41093
41153
  page = refreshedPage;
41094
- const refreshedSelectedIndex = selectedSessionId ? visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery }).findIndex((session) => session.id === selectedSessionId) : -1;
41154
+ const refreshedSelectedIndex = selectedSessionId ? page.sessions.findIndex((session) => session.id === selectedSessionId) : -1;
41095
41155
  selectedIndex = refreshedSelectedIndex >= 0 ? refreshedSelectedIndex : Math.max(0, Math.min(selectedIndex, page.sessions.length - 1));
41096
41156
  pageError = undefined;
41097
41157
  render();
41098
41158
  } catch (error93) {
41099
- if (!destroyed && !settled) {
41159
+ if (!destroyed && !settled && requestVersion === pageRequestVersion) {
41100
41160
  pageError = error93 instanceof Error ? error93.message : String(error93);
41101
41161
  render();
41102
41162
  }
41103
41163
  } finally {
41104
- pageRequestInFlight = false;
41164
+ if (requestVersion === pageRequestVersion)
41165
+ pageRequestInFlight = false;
41105
41166
  }
41106
41167
  }
41107
41168
  async function applyFilters(nextFilters) {
41108
- if (loadingPage)
41109
- return;
41169
+ const appliedFilterMode = filterMode;
41170
+ const requestVersion = ++pageRequestVersion;
41171
+ pageRequestInFlight = true;
41110
41172
  loadingPage = true;
41111
41173
  pageError = undefined;
41112
- authorError = undefined;
41113
41174
  render();
41114
41175
  try {
41115
- page = await loadPage({ filters: nextFilters, signal: loadPageAbortController.signal });
41176
+ const loadedPage = await loadPage({ filters: nextFilters, signal: loadPageAbortController.signal });
41177
+ if (destroyed || settled || requestVersion !== pageRequestVersion)
41178
+ return;
41179
+ page = loadedPage;
41116
41180
  filters = nextFilters;
41117
41181
  pageIndex = 1;
41118
41182
  selectedIndex = 0;
41119
- filterMode = undefined;
41183
+ if (filterMode === appliedFilterMode)
41184
+ filterMode = undefined;
41120
41185
  } catch (error93) {
41121
- pageError = error93 instanceof Error ? error93.message : String(error93);
41186
+ if (!destroyed && !settled && requestVersion === pageRequestVersion)
41187
+ pageError = error93 instanceof Error ? error93.message : String(error93);
41122
41188
  } finally {
41123
- loadingPage = false;
41124
- render();
41189
+ if (requestVersion === pageRequestVersion) {
41190
+ pageRequestInFlight = false;
41191
+ loadingPage = false;
41192
+ if (!destroyed && !settled)
41193
+ render();
41194
+ }
41195
+ }
41196
+ }
41197
+ async function loadRepositoryOptions() {
41198
+ if (repositoriesLoaded || optionStates.repository.loading || !loadRepositories)
41199
+ return;
41200
+ optionStates.repository.loading = true;
41201
+ optionStates.repository.error = undefined;
41202
+ render();
41203
+ try {
41204
+ const loaded = await loadRepositories();
41205
+ if (destroyed || settled)
41206
+ return;
41207
+ repositories = loaded;
41208
+ repositoriesLoaded = true;
41209
+ } catch (error93) {
41210
+ if (!destroyed && !settled)
41211
+ optionStates.repository.error = error93 instanceof Error ? error93.message : String(error93);
41212
+ } finally {
41213
+ optionStates.repository.loading = false;
41214
+ if (!destroyed && !settled)
41215
+ render();
41125
41216
  }
41126
41217
  }
41127
41218
  async function openAuthorFilter() {
41128
41219
  filterMode = createFilterMode({ kind: "creator", filters });
41129
- authorError = undefined;
41220
+ optionStates.creator.error = undefined;
41130
41221
  render();
41131
- if (authorsLoaded || loadingAuthors || !loadAuthors)
41222
+ if (authorsLoaded || optionStates.creator.loading || !loadAuthors)
41132
41223
  return;
41133
- loadingAuthors = true;
41224
+ optionStates.creator.loading = true;
41134
41225
  render();
41135
41226
  try {
41136
- authors = await loadAuthors();
41227
+ const loaded = await loadAuthors();
41228
+ if (destroyed || settled)
41229
+ return;
41230
+ authors = loaded;
41137
41231
  authorsLoaded = true;
41138
41232
  if (filterMode?.kind === "creator") {
41139
41233
  const firstSelectedIndex = authors.findIndex((author) => filterMode?.creators.some((creator) => creator.id === author.id));
41140
41234
  filterMode = { ...filterMode, selectedIndex: Math.max(0, firstSelectedIndex) };
41141
41235
  }
41142
41236
  } catch (error93) {
41143
- authorError = error93 instanceof Error ? error93.message : String(error93);
41237
+ if (!destroyed && !settled)
41238
+ optionStates.creator.error = error93 instanceof Error ? error93.message : String(error93);
41144
41239
  } finally {
41145
- loadingAuthors = false;
41146
- render();
41240
+ optionStates.creator.loading = false;
41241
+ if (!destroyed && !settled)
41242
+ render();
41147
41243
  }
41148
41244
  }
41149
41245
  async function moveSelection(direction) {
41150
- const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
41246
+ const sessions = page.sessions;
41151
41247
  if (loadingPage || sessions.length === 0)
41152
41248
  return;
41153
41249
  const isBoundary = direction === "next" ? selectedIndex === sessions.length - 1 : selectedIndex === 0;
@@ -41158,6 +41254,12 @@ async function createDashboardTui({
41158
41254
  }
41159
41255
  await loadAdjacentPage(direction);
41160
41256
  }
41257
+ async function loadSessionSearch() {
41258
+ clearTimeout(searchTimer);
41259
+ searchTimer = undefined;
41260
+ const search = sessionSearchQuery.trim();
41261
+ await applyFilters({ ...filters, search: search || undefined });
41262
+ }
41161
41263
  inputHandler = (sequence) => {
41162
41264
  if (sequence === "\x03") {
41163
41265
  finish({ kind: "interrupt" });
@@ -41236,7 +41338,8 @@ function formatSessionRow({ session, selected, width }) {
41236
41338
  const title = truncateText(session.title ?? "Untitled session", Math.max(8, Math.min(40, Math.floor(width * 0.24))));
41237
41339
  const attribution = `${session.creator} (${session.source})`;
41238
41340
  const metadataWidth = Math.max(0, width - Bun.stringWidth(prefix + title + activity.label) - 4);
41239
- const metadata = truncateText(`${attribution} \xB7 ${outcome} \xB7 ${updated}`, metadataWidth);
41341
+ const work = session.repositories.length > 0 ? `${sessionRepositoryBranches(session, Math.max(0, Math.floor(metadataWidth * 0.6)))} \xB7 ` : "";
41342
+ const metadata = truncateText(`${work}${attribution} \xB7 ${outcome} \xB7 ${updated}`, metadataWidth);
41240
41343
  const line = [
41241
41344
  fg7(PALETTE.dimText)(prefix),
41242
41345
  fg7(PALETTE.bodyText)(title),
@@ -41264,14 +41367,18 @@ function createFilterMode({ kind, filters }) {
41264
41367
  selectedIndex: 0,
41265
41368
  statuses: [...filters.statuses],
41266
41369
  creators: [...filters.creators],
41370
+ repositories: [...filters.repositories],
41371
+ branchName: filters.branchName,
41267
41372
  query: "",
41268
41373
  searching: false
41269
41374
  };
41270
41375
  }
41271
- function formatFilterMenu({ mode, authors, width }) {
41272
- const options = visibleDashboardFilterOptions({ mode, authors });
41273
- const selectedCount = mode.kind === "status" ? mode.statuses.length : mode.creators.length;
41274
- const heading = `${mode.kind === "status" ? "Filter by status" : "Filter by author"} \xB7 ${selectedCount} selected \xB7 empty shows all`;
41376
+ function formatFilterMenu({ mode, authors, repositories, width }) {
41377
+ if (mode.kind === "branch")
41378
+ return stringToStyledText2("Filter by branch \xB7 contains text \xB7 empty shows all");
41379
+ const options = visibleDashboardFilterOptions({ mode, authors, repositories });
41380
+ const selectedCount = mode.kind === "status" ? mode.statuses.length : mode.kind === "repository" ? mode.repositories.length : mode.creators.length;
41381
+ const heading = `${mode.kind === "status" ? "Filter by status" : mode.kind === "repository" ? "Filter by repository" : "Filter by author"} \xB7 ${selectedCount} selected \xB7 empty shows all`;
41275
41382
  const rows = options.map((option, index) => {
41276
41383
  const active = isDashboardFilterOptionSelected({ mode, option });
41277
41384
  const label = `${active ? "[x]" : "[ ]"} ${truncateText(option.label, Math.max(1, width - 4))}`;
@@ -41286,16 +41393,16 @@ function formatFilterMenu({ mode, authors, width }) {
41286
41393
  ], `
41287
41394
  `);
41288
41395
  }
41289
- function dashboardFilterOptions({ kind, authors }) {
41290
- return kind === "status" ? lifecycleStatusOptions.map((option) => ({ kind: "status", id: option.value, label: option.label })) : authors.map((author) => ({ kind: "creator", id: author.id, label: author.name }));
41396
+ function dashboardFilterOptions({ kind, authors, repositories }) {
41397
+ return kind === "status" ? lifecycleStatusOptions.map((option) => ({ kind: "status", id: option.value, label: option.label })) : kind === "repository" ? repositories.map((repository) => ({ kind: "repository", id: repository.id, label: repository.fullName })) : authors.map((author) => ({ kind: "creator", id: author.id, label: author.name }));
41291
41398
  }
41292
- function visibleDashboardFilterOptions({ mode, authors }) {
41293
- return dashboardFilterOptions({ kind: mode.kind, authors }).filter((option) => fuzzyMatches(option.label, mode.query));
41399
+ function visibleDashboardFilterOptions({ mode, authors, repositories }) {
41400
+ return dashboardFilterOptions({ kind: mode.kind, authors, repositories }).filter((option) => fuzzyMatches(option.label, mode.query));
41294
41401
  }
41295
41402
  function isDashboardFilterOptionSelected({ mode, option }) {
41296
- return option.kind === "status" ? mode.statuses.includes(option.id) : mode.creators.some((creator) => creator.id === option.id);
41403
+ return option.kind === "status" ? mode.statuses.includes(option.id) : option.kind === "repository" ? mode.repositories.some((repository) => repository.id === option.id) : mode.creators.some((creator) => creator.id === option.id);
41297
41404
  }
41298
- function updateDashboardFilterSelection({ mode, option, selected, authors }) {
41405
+ function updateDashboardFilterSelection({ mode, option, selected, authors, repositories }) {
41299
41406
  if (option.kind === "status") {
41300
41407
  const selectedStatuses = new Set(mode.statuses);
41301
41408
  if (selected)
@@ -41307,6 +41414,14 @@ function updateDashboardFilterSelection({ mode, option, selected, authors }) {
41307
41414
  statuses: lifecycleStatusOptions.map((entry2) => entry2.value).filter((status) => selectedStatuses.has(status))
41308
41415
  };
41309
41416
  }
41417
+ if (option.kind === "repository") {
41418
+ const ids = new Set(mode.repositories.map((repository) => repository.id));
41419
+ if (selected)
41420
+ ids.add(option.id);
41421
+ else
41422
+ ids.delete(option.id);
41423
+ return { ...mode, repositories: repositories.filter((repository) => ids.has(repository.id)).map(({ id, fullName }) => ({ id, fullName })) };
41424
+ }
41310
41425
  const selectedCreatorIds = new Set(mode.creators.map((creator) => creator.id));
41311
41426
  if (selected)
41312
41427
  selectedCreatorIds.add(option.id);
@@ -41334,7 +41449,7 @@ function formatSessionPreview({ session, width }) {
41334
41449
  fg7(PALETTE.dimText)(" \xB7 "),
41335
41450
  bold3(fg7(activity.color)(activity.label))
41336
41451
  ],
41337
- [fg7(PALETTE.dimText)(truncateText(`Work: ${sessionWork(session)}`, width))],
41452
+ [fg7(PALETTE.dimText)(truncateText(`Work: ${sessionWork(session, width)}`, width))],
41338
41453
  [fg7(PALETTE.bodyText)(truncateText(`Outcome: ${sessionOutcome(session)}`, width))],
41339
41454
  [fg7(PALETTE.dimText)(truncateText(`Created by: ${session.creator} \xB7 Updated ${formatUpdatedAt(session.updatedAt ?? session.createdAt)}`, width))]
41340
41455
  ], `
@@ -41345,7 +41460,7 @@ function formatSessionCard({ session, selectedIndex, sessionCount, width }) {
41345
41460
  const lines = [
41346
41461
  `Session #${session.sessionNumber} \xB7 ${selectedIndex + 1} of ${sessionCount}`,
41347
41462
  truncateText(session.title ?? "Untitled session", width),
41348
- truncateText(`Work: ${sessionWork(session)}`, width),
41463
+ truncateText(`Work: ${sessionWork(session, width)}`, width),
41349
41464
  `Now: ${activity.label}`,
41350
41465
  truncateText(`Outcome: ${sessionOutcome(session)}`, width),
41351
41466
  truncateText(`Created by: ${session.creator} \xB7 Updated ${formatUpdatedAt(session.updatedAt ?? session.createdAt)}`, width)
@@ -41378,9 +41493,33 @@ function sessionActivityPresentation(session) {
41378
41493
  return { ...agent, label: "Ready" };
41379
41494
  return agent;
41380
41495
  }
41381
- function sessionWork(session) {
41382
- const repositories = session.repositories.map((repository) => repository.fullName).join(", ") || "No repository selected";
41383
- return `${repositories} \xB7 ${session.source}`;
41496
+ function prioritizeSessionMatches(session, filters, query) {
41497
+ const filteredRepositories = session.repositories.filter((repository2) => (filters.repositories.length === 0 || filters.repositories.some((selected) => selected.id === repository2.id)) && (!filters.branchName || repository2.branchName?.toLowerCase().includes(filters.branchName.toLowerCase())));
41498
+ const repository = filteredRepositories.find((repository2) => fuzzyMatches(repository2.fullName, query) || fuzzyMatches(repository2.branchName ?? "", query)) ?? filteredRepositories[0];
41499
+ const pullRequest = session.pullRequests.find((pullRequest2) => matchesPullRequestSearch(pullRequest2, query));
41500
+ return {
41501
+ ...session,
41502
+ repositories: repository ? [repository, ...session.repositories.filter((item) => item !== repository)] : session.repositories,
41503
+ pullRequests: pullRequest ? [pullRequest, ...session.pullRequests.filter((item) => item !== pullRequest)] : session.pullRequests
41504
+ };
41505
+ }
41506
+ function matchesPullRequestSearch(pullRequest, query) {
41507
+ return fuzzyMatches(`#${pullRequest.number}`, query) || fuzzyMatches(pullRequest.title, query);
41508
+ }
41509
+ function sessionRepositoryBranches(session, width) {
41510
+ const repository = session.repositories[0];
41511
+ if (!repository)
41512
+ return truncateText("No repository selected", width);
41513
+ const more = session.repositories.length > 1 ? ` +${session.repositories.length - 1}` : "";
41514
+ const available = Math.max(0, width - Bun.stringWidth(more) - 3);
41515
+ const name = truncateText(repository.fullName, Math.floor(available / 2));
41516
+ const branch = truncateText(repository.branchName ?? "Branch pending", Math.max(0, available - Bun.stringWidth(name)));
41517
+ return `${name} \xB7 ${branch}${more}`;
41518
+ }
41519
+ function sessionWork(session, width) {
41520
+ const source = truncateText(session.source, Math.floor(width / 4));
41521
+ const repositories = sessionRepositoryBranches(session, Math.max(0, width - Bun.stringWidth(source) - 9));
41522
+ return `${repositories} \xB7 ${source}`;
41384
41523
  }
41385
41524
  function sessionOutcome(session) {
41386
41525
  const first = session.pullRequests[0];
@@ -41403,14 +41542,8 @@ function formatUpdatedAt(value) {
41403
41542
  return `${Math.floor(seconds / (60 * 60))}h ago`;
41404
41543
  return new Date(milliseconds).toLocaleDateString(undefined, { month: "short", day: "numeric" });
41405
41544
  }
41406
- function matchesSessionSearch({ session, query }) {
41407
- return fuzzyMatches(session.id, query) || fuzzyMatches(session.title ?? "", query) || fuzzyMatches(String(session.sessionNumber), query);
41408
- }
41409
- function visibleDashboardSessions({ sessions, query }) {
41410
- return sessions.filter((session) => matchesSessionSearch({ session, query }));
41411
- }
41412
41545
  function dashboardStatusBand({ page, pageIndex, filters, sessionSearchQuery }) {
41413
- const openCount = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery }).filter((session) => session.status === "open").length;
41546
+ const openCount = page.sessions.filter((session) => session.status === "open").length;
41414
41547
  const paging = [`page ${pageIndex}`];
41415
41548
  if (page.canGoPrevious)
41416
41549
  paging.push("\u2190 prev");
@@ -41419,12 +41552,14 @@ function dashboardStatusBand({ page, pageIndex, filters, sessionSearchQuery }) {
41419
41552
  const activeFilters = [
41420
41553
  filters.statuses.length > 0 ? `status: ${filters.statuses.map(sessionStatusLabel).join(", ")}` : undefined,
41421
41554
  filters.creators.length > 0 ? `author: ${filters.creators.map((creator) => creator.name).join(", ")}` : undefined,
41555
+ filters.repositories.length > 0 ? `repo: ${filters.repositories.map((repository) => repository.fullName).join(", ")}` : undefined,
41556
+ filters.branchName ? `branch: ${filters.branchName}` : undefined,
41422
41557
  sessionSearchQuery ? `search: ${sessionSearchQuery}` : undefined
41423
41558
  ].filter((value) => value !== undefined);
41424
41559
  return `remy \xB7 sessions \xB7 ${openCount} open${activeFilters.length > 0 ? ` \xB7 ${activeFilters.join(" \xB7 ")}` : ""} \xB7 ${paging.join(" \xB7 ")}`;
41425
41560
  }
41426
41561
  function hasActiveFilters(filters) {
41427
- return filters.statuses.length > 0 || filters.creators.length > 0;
41562
+ return filters.statuses.length > 0 || filters.creators.length > 0 || filters.repositories.length > 0 || filters.branchName.length > 0;
41428
41563
  }
41429
41564
  function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen, sessionSearchOpen, width }) {
41430
41565
  if (logoutConfirmationOpen)
@@ -41440,19 +41575,24 @@ function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmation
41440
41575
  parts.push(`\u2193 ${rowsBelow} more below`);
41441
41576
  return joinStyled([
41442
41577
  ...parts.length > 0 ? [[fg7(pageError ? PALETTE.failure : PALETTE.dimText)(parts.join(" \xB7 "))]] : [],
41443
- renderKeyHints({ hints: [{ key: "\u2191\u2193", action: "move" }, { key: "\u23CE", action: "open" }, { key: "s", action: "search" }, { key: "f", action: "status" }, { key: "a", action: "author" }, { key: "n", action: "new session" }, { key: "\u21E7l", action: "log out" }, { key: "q", action: "quit" }], width })
41578
+ renderKeyHints({ hints: [{ key: "\u2191\u2193", action: "move" }, { key: "\u23CE", action: "open" }, { key: "s", action: "search" }, { key: "f", action: "status" }, { key: "a", action: "author" }, { key: "r", action: "repository" }, { key: "b", action: "branch" }, { key: "n", action: "new session" }, { key: "\u21E7l", action: "log out" }, { key: "q", action: "quit" }], width })
41444
41579
  ], `
41445
41580
  `);
41446
41581
  }
41447
- function dashboardFilterFooter({ mode, loadingPage, loadingAuthors, pageError, authorError, width }) {
41448
- if (loadingAuthors)
41449
- return joinStyled([[fg7(PALETTE.progress)("Loading authors\u2026")], renderKeyHints({ hints: [{ key: "Esc", action: "cancel" }], width })], " \xB7 ");
41582
+ function dashboardFilterFooter({ mode, loadingPage, loadingOptions, pageError, optionError, width }) {
41583
+ if (loadingOptions)
41584
+ return joinStyled([[fg7(PALETTE.progress)("Loading filter options\u2026")], renderKeyHints({ hints: [{ key: "Esc", action: "cancel" }], width })], " \xB7 ");
41450
41585
  if (loadingPage)
41451
41586
  return stringToStyledText2("Applying filter\u2026");
41452
- if (mode.searching)
41453
- return renderKeyHints({ hints: [{ key: "type", action: "to filter" }, { key: "\u23CE", action: "apply search" }, { key: "Esc", action: "cancel search" }], width });
41587
+ if (mode.searching) {
41588
+ return joinStyled([
41589
+ ...pageError ? [[fg7(PALETTE.failure)(`Could not apply filter: ${pageError}`)]] : [],
41590
+ renderKeyHints({ hints: [{ key: "type", action: "to filter" }, { key: "\u23CE", action: mode.kind === "branch" ? "apply branch" : "apply search" }, { key: "Esc", action: "cancel" }], width })
41591
+ ], `
41592
+ `);
41593
+ }
41454
41594
  return joinStyled([
41455
- ...authorError ? [[fg7(PALETTE.failure)(`Could not load authors: ${authorError}`)]] : [],
41595
+ ...optionError ? [[fg7(PALETTE.failure)(`Could not load filter options: ${optionError}`)]] : [],
41456
41596
  ...pageError ? [[fg7(PALETTE.failure)(`Could not apply filter: ${pageError}`)]] : [],
41457
41597
  renderKeyHints({ hints: [{ key: "\u2191\u2193/jk", action: "move" }, { key: "space", action: "toggle" }, { key: "\u2190\u2192/hl", action: "mark" }, { key: "s", action: "search" }, { key: "\u23CE", action: "apply" }, { key: "Esc", action: "cancel" }], width })
41458
41598
  ], `
@@ -41463,7 +41603,7 @@ async function createDefaultRenderer() {
41463
41603
  }
41464
41604
 
41465
41605
  // src/tui/new-session-wizard.ts
41466
- import { bg as bg2, bold as bold4, CliRenderEvents as CliRenderEvents4, dim as dim6, fg as fg11, StyledText as StyledText10, stringToStyledText as stringToStyledText3, TextRenderable as TextRenderable4 } from "@opentui/core";
41606
+ import { bg as bg4, bold as bold4, CliRenderEvents as CliRenderEvents4, dim as dim6, fg as fg11, StyledText as StyledText10, stringToStyledText as stringToStyledText3, TextRenderable as TextRenderable4 } from "@opentui/core";
41467
41607
 
41468
41608
  // src/tui/attachment-presentation.ts
41469
41609
  import { dim as dim4, fg as fg9, StyledText as StyledText8 } from "@opentui/core";
@@ -42941,7 +43081,7 @@ function renderStepIndicator({ step, hasInstallationChoice, hasBranchChoice = fa
42941
43081
  function renderSelectableRow({ label, isCursor }) {
42942
43082
  const line = `${isCursor ? "> " : " "}${label}`;
42943
43083
  const styled = fg11(PALETTE.bodyText)(line);
42944
- return [isCursor ? bg2(PALETTE.selectionBg)(styled) : styled];
43084
+ return [isCursor ? bg4(PALETTE.selectionBg)(styled) : styled];
42945
43085
  }
42946
43086
  function renderRepositoryRow({ repository, isCursor, isChecked, source }) {
42947
43087
  const marker = isChecked ? "[x]" : "[ ]";
@@ -42965,7 +43105,7 @@ async function createDefaultRenderer2() {
42965
43105
  }
42966
43106
 
42967
43107
  // src/tui/session-view.ts
42968
- import { CliRenderEvents as CliRenderEvents5, bg as bg3, bold as bold5, dim as dim7, fg as fg12, link as link2, StyledText as StyledText11, stringToStyledText as stringToStyledText5, TextRenderable as TextRenderable5 } from "@opentui/core";
43108
+ import { CliRenderEvents as CliRenderEvents5, bg as bg5, bold as bold5, dim as dim7, fg as fg12, link as link2, StyledText as StyledText11, stringToStyledText as stringToStyledText5, TextRenderable as TextRenderable5 } from "@opentui/core";
42969
43109
 
42970
43110
  // src/tui/session-mcps-view.ts
42971
43111
  import { stringToStyledText as stringToStyledText4 } from "@opentui/core";
@@ -43344,7 +43484,7 @@ async function createSessionTui({
43344
43484
  `), joinStyled(liveContent, `
43345
43485
 
43346
43486
  `)], ""));
43347
- composer.placeholder = lifecycleRequestState.kind === "pending" ? `${lifecycleOperationPresentParticiple(lifecycleRequestState.operation)} the session \u2014 messages are paused.` : renderer.height <= 10 ? "Message Remy\u2026 \xB7 Enter sends" : idleComposerPlaceholder;
43487
+ composer.placeholder = latestState.historyStatus === "loading" ? "Draft a message\u2026 \xB7 sending waits for history" : lifecycleRequestState.kind === "pending" ? `${lifecycleOperationPresentParticiple(lifecycleRequestState.operation)} the session \u2014 messages are paused.` : renderer.height <= 10 ? "Message Remy\u2026 \xB7 Enter sends" : idleComposerPlaceholder;
43348
43488
  resizeComposer(composer);
43349
43489
  if (latestState.aggregateStatus === "open")
43350
43490
  composerLayout.render();
@@ -43353,6 +43493,7 @@ async function createSessionTui({
43353
43493
  const commandMenu = renderSlashCommandMenu({ commands: slashCompletions, selectedIndex: slashCompletionIndex, width: renderer.width - 2, maxRows: completionMenuRows(renderer.height) });
43354
43494
  const agentSettingsMenu = agentSettingsPicker && latestState.aggregateStatus === "open" ? renderAgentSettingsPicker({ picker: agentSettingsPicker, width: renderer.width - 2, maxRows: completionMenuRows(renderer.height) }) : undefined;
43355
43495
  const composerStatus = [
43496
+ latestState.historyStatus === "loading" ? renderFeedback({ kind: "progress", message: "Loading conversation\u2026 You can draft now; sending is paused." }) : undefined,
43356
43497
  composerDraft.workspaceContexts?.length ? renderFeedback({ kind: "info", message: `${composerDraft.workspaceContexts.length} workspace context(s) \xB7 /context to review/remove` }) : undefined,
43357
43498
  attachmentFeedback ? renderAttachmentFeedback(attachmentFeedback) : undefined,
43358
43499
  composerFeedback ? renderFeedback(composerFeedback) : undefined,
@@ -44016,6 +44157,12 @@ ${segment.excerpt}`).join(`
44016
44157
  render();
44017
44158
  }
44018
44159
  }
44160
+ transcriptScrollBox.onMouseDown = (event) => {
44161
+ if (latestState.aggregateStatus === "open" && event.button === 0) {
44162
+ event.preventDefault();
44163
+ composer.focus();
44164
+ }
44165
+ };
44019
44166
  for (const text of [transcript, liveTranscript]) {
44020
44167
  let pressedLink;
44021
44168
  text.onMouseDown = (event) => {
@@ -44069,6 +44216,10 @@ ${segment.excerpt}`).join(`
44069
44216
  }
44070
44217
  if (!ownsAttachmentPreparation(preparationOwner))
44071
44218
  return;
44219
+ if (latestState.historyStatus === "loading") {
44220
+ invalidateAttachmentPreparation();
44221
+ return;
44222
+ }
44072
44223
  if (lifecycleRequestState.kind === "pending") {
44073
44224
  composerFeedback = { kind: "progress", message: `${lifecycleOperationPresentParticiple(lifecycleRequestState.operation)} the session \u2014 messages are paused.` };
44074
44225
  render();
@@ -44535,8 +44686,8 @@ function renderTerminalSessionBand({ aggregateStatus, width }) {
44535
44686
  const status = sessionStatusLabel(aggregateStatus).toLowerCase();
44536
44687
  return joinStyled([
44537
44688
  new StyledText11([
44538
- bg3(PALETTE.selectionBg)(bold5(fg12(PALETTE.systemAccent)(` Session ${status} `))),
44539
- bg3(PALETTE.selectionBg)(fg12(PALETTE.bodyText)("\u2014 this conversation is closed. "))
44689
+ bg5(PALETTE.selectionBg)(bold5(fg12(PALETTE.systemAccent)(` Session ${status} `))),
44690
+ bg5(PALETTE.selectionBg)(fg12(PALETTE.bodyText)("\u2014 this conversation is closed. "))
44540
44691
  ]),
44541
44692
  renderKeyHints({ hints: [{ key: "esc", action: "return to dashboard" }, { key: "n", action: "new session" }, { key: "m", action: "MCPs" }, { key: "ctrl+f", action: "files" }, { key: "ctrl+l", action: "links" }], width })
44542
44693
  ], `
@@ -45245,7 +45396,7 @@ function isPng(bytes) {
45245
45396
  }
45246
45397
 
45247
45398
  // src/tui/remy-splash.ts
45248
- import { bold as bold6, BoxRenderable as BoxRenderable5, fg as fg13, NativeImage, StyledText as StyledText12, TextRenderable as TextRenderable6, bg as bg4 } from "@opentui/core";
45399
+ import { bold as bold6, BoxRenderable as BoxRenderable5, fg as fg13, NativeImage, StyledText as StyledText12, TextRenderable as TextRenderable6, bg as bg6 } from "@opentui/core";
45249
45400
 
45250
45401
  // src/tui/remy-mark.ts
45251
45402
  var remyPixelFieldSource = new URL("../assets/remy-pixel-field.png", import.meta.url);
@@ -45519,7 +45670,7 @@ function renderMark(field, frame, rows = markRows) {
45519
45670
  const flush = () => {
45520
45671
  if (runLength === 0)
45521
45672
  return;
45522
- chunks.push(fg13(grey(runTop))(bg4(grey(runBottom))(upperHalfBlock.repeat(runLength))));
45673
+ chunks.push(fg13(grey(runTop))(bg6(grey(runBottom))(upperHalfBlock.repeat(runLength))));
45523
45674
  runLength = 0;
45524
45675
  };
45525
45676
  for (let column = 0;column < columns; column++) {
@@ -46091,7 +46242,7 @@ async function dashboard({
46091
46242
  const openDashboardTui = dependencies.openDashboardTui ?? createDashboardTui;
46092
46243
  const pages = [initialSessions];
46093
46244
  let pageIndex = 0;
46094
- let filters = { statuses: [], creators: [] };
46245
+ let filters = { statuses: [], creators: [], repositories: [], branchName: "" };
46095
46246
  let authors;
46096
46247
  let activeDashboardGeneration = 0;
46097
46248
  function holdTransitionScreen() {
@@ -46100,8 +46251,9 @@ async function dashboard({
46100
46251
  function createDashboardPageLoader({ generation }) {
46101
46252
  const loadingPages = new Map;
46102
46253
  let pageLoadTail = Promise.resolve();
46254
+ let filterRequestNumber = 0;
46103
46255
  return async (input) => {
46104
- const requestKey = "filters" in input ? `filters:${JSON.stringify(input.filters)}` : input.target;
46256
+ const requestKey = "filters" in input ? `filters:${++filterRequestNumber}` : input.target;
46105
46257
  const matchingPageLoad = loadingPages.get(requestKey);
46106
46258
  if (matchingPageLoad)
46107
46259
  return await matchingPageLoad;
@@ -46116,6 +46268,9 @@ async function dashboard({
46116
46268
  limit: 20,
46117
46269
  ...nextFilters.statuses.length > 0 ? { statuses: nextFilters.statuses } : {},
46118
46270
  ...nextFilters.creators.length > 0 ? { creatorUserIds: nextFilters.creators.map((creator) => creator.id) } : {},
46271
+ ...nextFilters.repositories.length > 0 ? { repositoryIds: nextFilters.repositories.map((repository) => repository.id) } : {},
46272
+ ...nextFilters.branchName ? { branchName: nextFilters.branchName } : {},
46273
+ ...nextFilters.search ? { search: nextFilters.search } : {},
46119
46274
  signal: input.signal
46120
46275
  }));
46121
46276
  if (generation !== activeDashboardGeneration)
@@ -46134,6 +46289,9 @@ async function dashboard({
46134
46289
  after,
46135
46290
  ...filters.statuses.length > 0 ? { statuses: filters.statuses } : {},
46136
46291
  ...filters.creators.length > 0 ? { creatorUserIds: filters.creators.map((creator) => creator.id) } : {},
46292
+ ...filters.repositories.length > 0 ? { repositoryIds: filters.repositories.map((repository) => repository.id) } : {},
46293
+ ...filters.branchName ? { branchName: filters.branchName } : {},
46294
+ ...filters.search ? { search: filters.search } : {},
46137
46295
  signal
46138
46296
  }));
46139
46297
  if (generation !== activeDashboardGeneration)
@@ -46154,6 +46312,9 @@ async function dashboard({
46154
46312
  after: current.nextCursor,
46155
46313
  ...filters.statuses.length > 0 ? { statuses: filters.statuses } : {},
46156
46314
  ...filters.creators.length > 0 ? { creatorUserIds: filters.creators.map((creator) => creator.id) } : {},
46315
+ ...filters.repositories.length > 0 ? { repositoryIds: filters.repositories.map((repository) => repository.id) } : {},
46316
+ ...filters.branchName ? { branchName: filters.branchName } : {},
46317
+ ...filters.search ? { search: filters.search } : {},
46157
46318
  signal
46158
46319
  }));
46159
46320
  if (generation !== activeDashboardGeneration)
@@ -46193,6 +46354,9 @@ async function dashboard({
46193
46354
  limit: 20,
46194
46355
  ...filters.statuses.length > 0 ? { statuses: filters.statuses } : {},
46195
46356
  ...filters.creators.length > 0 ? { creatorUserIds: filters.creators.map((creator) => creator.id) } : {},
46357
+ ...filters.repositories.length > 0 ? { repositoryIds: filters.repositories.map((repository) => repository.id) } : {},
46358
+ ...filters.branchName ? { branchName: filters.branchName } : {},
46359
+ ...filters.search ? { search: filters.search } : {},
46196
46360
  signal: dependencies.abortSignal
46197
46361
  })));
46198
46362
  pageIndex = 0;
@@ -46203,6 +46367,7 @@ async function dashboard({
46203
46367
  const action = await renderRemyView({
46204
46368
  open: async () => await openDashboardTui({
46205
46369
  initialPage: pages[0],
46370
+ loadRepositories: async () => await repositories,
46206
46371
  initialFilters: filters,
46207
46372
  loadAuthors: loadDashboardAuthors,
46208
46373
  loadPage: loadDashboardPage
@@ -46731,6 +46896,9 @@ async function runAttachedSession({
46731
46896
  const aborted3 = interactive ? new Promise((_resolve, reject) => {
46732
46897
  rejectAborted = reject;
46733
46898
  }) : undefined;
46899
+ aborted3?.catch(() => {
46900
+ return;
46901
+ });
46734
46902
  const abortHandler = () => {
46735
46903
  stop();
46736
46904
  rejectAborted(dependencies.abortSignal?.reason instanceof Error ? dependencies.abortSignal.reason : new Error("interrupted"));
@@ -46747,11 +46915,12 @@ async function runAttachedSession({
46747
46915
  });
46748
46916
  }
46749
46917
  const startPromise = controller.start(start);
46750
- await Promise.race([
46751
- controller.waitUntilReady(),
46752
- startPromise,
46753
- ...aborted3 ? [aborted3] : []
46754
- ]);
46918
+ const controllerFailed = startPromise.then(async () => await new Promise(() => {}));
46919
+ controllerFailed.catch(() => {
46920
+ return;
46921
+ });
46922
+ if (!interactive)
46923
+ await Promise.race([controller.waitUntilReady(), startPromise, ...aborted3 ? [aborted3] : []]);
46755
46924
  if (interactive) {
46756
46925
  if (dependencies.abortSignal?.aborted)
46757
46926
  throw dependencies.abortSignal.reason ?? new Error("interrupted");
@@ -46824,7 +46993,7 @@ async function runAttachedSession({
46824
46993
  savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir2() }),
46825
46994
  attachPromptPaths: async (input) => await uploadPromptPaths({ operations, ...input })
46826
46995
  }),
46827
- waitForAction: async (tui) => await Promise.race([tui.waitForAction(), aborted3])
46996
+ waitForAction: async (tui) => await Promise.race([tui.waitForAction(), aborted3, controllerFailed])
46828
46997
  });
46829
46998
  if (!action)
46830
46999
  throw dependencies.abortSignal?.reason ?? new Error("interrupted");
@@ -46911,7 +47080,7 @@ async function runAttachedSession({
46911
47080
  view.invalidate(event);
46912
47081
  }
46913
47082
  mountedView = view;
46914
- return await Promise.race([view.waitForAction(), aborted3]);
47083
+ return await Promise.race([view.waitForAction(), aborted3, controllerFailed]);
46915
47084
  }
46916
47085
  });
46917
47086
  if (!result2 || result2.kind === "interrupt")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/remy-cli",
3
- "version": "1.25.1-rc.0",
3
+ "version": "1.26.0-rc.0",
4
4
  "description": "Remy, the Coding Agent terminal client.",
5
5
  "type": "module",
6
6
  "bin": {