@meistrari/remy-cli 1.13.0 → 1.14.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 +18 -7
  2. package/dist/remy.js +1506 -397
  3. package/package.json +1 -1
package/dist/remy.js CHANGED
@@ -18,8 +18,8 @@ var __export = (target, all) => {
18
18
  // src/commands.ts
19
19
  import { randomUUID as randomUUID5 } from "crypto";
20
20
  import { spawn } from "child_process";
21
- import { mkdir as mkdir5, readFile as readFile5, readdir as readdir2, rename as rename4, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
22
- import { tmpdir } from "os";
21
+ import { mkdir as mkdir5, readFile as readFile5, readdir as readdir3, rename as rename4, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
22
+ import { tmpdir as tmpdir2 } from "os";
23
23
  import { dirname as dirname6, join as join6 } from "path";
24
24
 
25
25
  // ../../node_modules/.bun/@meistrari+auth-cli@1.6.1+aff0271b2b55d60c/node_modules/@meistrari/auth-cli/dist/index.mjs
@@ -31673,6 +31673,31 @@ var repositorySuggestionsInputSchema = exports_external2.strictObject({
31673
31673
  installationId: exports_external2.string().min(1),
31674
31674
  prompt: exports_external2.string().trim().min(1).max(20000)
31675
31675
  });
31676
+ var branchSuggestionsInputSchema = exports_external2.strictObject({
31677
+ installationId: exports_external2.string().min(1),
31678
+ prompt: exports_external2.string().trim().min(1).max(20000),
31679
+ repositoryIds: exports_external2.array(exports_external2.string().min(1)).min(1).max(20)
31680
+ });
31681
+ var branchSuggestionsResponseSchema = exports_external2.strictObject({
31682
+ repositories: exports_external2.array(exports_external2.discriminatedUnion("suggestion_kind", [
31683
+ exports_external2.strictObject({
31684
+ repository_id: exports_external2.string().min(1),
31685
+ suggestion_kind: exports_external2.literal("continue_pull_request"),
31686
+ branch_name: exports_external2.string().min(1),
31687
+ pull_request: exports_external2.strictObject({
31688
+ number: exports_external2.number().int().positive(),
31689
+ title: exports_external2.string(),
31690
+ url: exports_external2.url()
31691
+ })
31692
+ }),
31693
+ exports_external2.strictObject({
31694
+ repository_id: exports_external2.string().min(1),
31695
+ suggestion_kind: exports_external2.literal("create_session_branch"),
31696
+ branch_name: exports_external2.null(),
31697
+ pull_request: exports_external2.null()
31698
+ })
31699
+ ]))
31700
+ });
31676
31701
  var repositorySuggestionsResponseSchema = exports_external2.discriminatedUnion("selection_kind", [
31677
31702
  exports_external2.object({
31678
31703
  selection_kind: exports_external2.literal("repositories"),
@@ -31757,19 +31782,95 @@ async function suggestRemoteRepositories({
31757
31782
  confidence: parsed.data.confidence
31758
31783
  };
31759
31784
  }
31785
+ async function suggestRemoteBranches({
31786
+ client,
31787
+ input
31788
+ }) {
31789
+ const parsedInput = branchSuggestionsInputSchema.parse(input);
31790
+ const response = await client.request("/v1/repositories/branch-suggestions", {
31791
+ method: "POST",
31792
+ headers: { "content-type": "application/json" },
31793
+ body: JSON.stringify({
31794
+ github_installation_id: parsedInput.installationId,
31795
+ prompt: parsedInput.prompt,
31796
+ repository_ids: parsedInput.repositoryIds
31797
+ })
31798
+ });
31799
+ let body;
31800
+ try {
31801
+ body = await response.json();
31802
+ } catch (error93) {
31803
+ throw new CodingAgentProtocolError("Branch suggestions response was not valid JSON.", { cause: error93 });
31804
+ }
31805
+ const parsed = branchSuggestionsResponseSchema.safeParse(body);
31806
+ if (!parsed.success)
31807
+ throw new CodingAgentProtocolError("Branch suggestions response did not match the public API contract.", { cause: parsed.error });
31808
+ return parsed.data.repositories.map((repository) => repository.suggestion_kind === "continue_pull_request" ? {
31809
+ suggestionKind: "continuePullRequest",
31810
+ repositoryId: repository.repository_id,
31811
+ branchName: repository.branch_name,
31812
+ pullRequest: repository.pull_request
31813
+ } : {
31814
+ suggestionKind: "createSessionBranch",
31815
+ repositoryId: repository.repository_id,
31816
+ branchName: null,
31817
+ pullRequest: null
31818
+ });
31819
+ }
31820
+
31821
+ // ../../packages/coding-agent-client/src/users.ts
31822
+ var remoteUserPageDtoSchema = exports_external2.object({
31823
+ data: exports_external2.array(exports_external2.object({
31824
+ id: exports_external2.string(),
31825
+ name: exports_external2.string(),
31826
+ email: exports_external2.email()
31827
+ })),
31828
+ has_more: exports_external2.boolean(),
31829
+ next_cursor: exports_external2.string().nullable()
31830
+ });
31831
+ async function listRemoteUsers({
31832
+ client,
31833
+ limit,
31834
+ after,
31835
+ signal
31836
+ }) {
31837
+ const searchParams = new URLSearchParams;
31838
+ if (limit !== undefined)
31839
+ searchParams.set("limit", String(limit));
31840
+ if (after)
31841
+ searchParams.set("after", after);
31842
+ const suffix = searchParams.size > 0 ? `?${searchParams.toString()}` : "";
31843
+ const response = await client.request(`/users${suffix}`, { signal });
31844
+ let body;
31845
+ try {
31846
+ body = await response.json();
31847
+ } catch (error93) {
31848
+ throw new CodingAgentProtocolError("User directory response was not valid JSON.", { cause: error93 });
31849
+ }
31850
+ const parsed = remoteUserPageDtoSchema.safeParse(body);
31851
+ if (!parsed.success)
31852
+ throw new CodingAgentProtocolError("User directory response did not match the API contract.", { cause: parsed.error });
31853
+ return {
31854
+ data: parsed.data.data.map((user) => ({ id: user.id, name: user.name, email: user.email })),
31855
+ hasMore: parsed.data.has_more,
31856
+ nextCursor: parsed.data.next_cursor
31857
+ };
31858
+ }
31760
31859
 
31761
31860
  // ../../packages/coding-agent-client/src/files.ts
31861
+ import { Buffer } from "buffer";
31762
31862
  import { createHash } from "crypto";
31763
31863
  import { open } from "fs/promises";
31764
31864
  import { basename } from "path";
31765
- var maxFileBytes = 20 * 1024 * 1024;
31865
+ var fileUploadMaxBytes = 20 * 1024 * 1024;
31866
+ var fileUploadMaxSizeLabel = `${fileUploadMaxBytes / (1024 * 1024)} MiB`;
31766
31867
  var defaultPollIntervalMs = 250;
31767
31868
  var sha256Schema = exports_external2.string().regex(/^[0-9a-f]{64}$/);
31768
31869
  var fileIdentitySchema = exports_external2.strictObject({
31769
31870
  id: exports_external2.string(),
31770
31871
  filename: exports_external2.string().min(1).max(255),
31771
31872
  media_type: exports_external2.string().min(1).max(255),
31772
- byte_size: exports_external2.number().int().min(0).max(maxFileBytes),
31873
+ byte_size: exports_external2.number().int().min(0).max(fileUploadMaxBytes),
31773
31874
  sha256: sha256Schema
31774
31875
  });
31775
31876
  var uploadSignedUrlSchema = exports_external2.strictObject({
@@ -31820,9 +31921,37 @@ var fileResponseSchema = exports_external2.union([
31820
31921
  var createFileRequestSchema = exports_external2.strictObject({
31821
31922
  filename: exports_external2.string().min(1).max(255),
31822
31923
  media_type: exports_external2.string().min(1).max(255),
31823
- byte_size: exports_external2.number().int().min(0).max(maxFileBytes),
31924
+ byte_size: exports_external2.number().int().min(0).max(fileUploadMaxBytes),
31824
31925
  sha256: sha256Schema
31825
31926
  });
31927
+ async function readFileHandleWithLimit({
31928
+ fileHandle,
31929
+ initialByteSize,
31930
+ maxBytes
31931
+ }) {
31932
+ const maximumBufferBytes = maxBytes + 1;
31933
+ let buffer = Buffer.allocUnsafe(Math.min(maximumBufferBytes, Math.max(1, initialByteSize + 1)));
31934
+ let byteLength = 0;
31935
+ while (true) {
31936
+ if (byteLength === buffer.byteLength) {
31937
+ if (byteLength > maxBytes)
31938
+ return { status: "limit_exceeded" };
31939
+ const nextBuffer = Buffer.allocUnsafe(Math.min(maximumBufferBytes, Math.max(buffer.byteLength * 2, byteLength + 1)));
31940
+ buffer.copy(nextBuffer, 0, 0, byteLength);
31941
+ buffer = nextBuffer;
31942
+ }
31943
+ const { bytesRead } = await fileHandle.read(buffer, byteLength, buffer.byteLength - byteLength, null);
31944
+ if (bytesRead === 0) {
31945
+ return {
31946
+ status: "complete",
31947
+ bytes: Buffer.from(buffer.subarray(0, byteLength))
31948
+ };
31949
+ }
31950
+ byteLength += bytesRead;
31951
+ if (byteLength > maxBytes)
31952
+ return { status: "limit_exceeded" };
31953
+ }
31954
+ }
31826
31955
  async function reserveAndUploadFile({
31827
31956
  client,
31828
31957
  filePath,
@@ -31907,13 +32036,18 @@ async function readLocalUploadFile({ filePath, mediaType }) {
31907
32036
  throw new LocalFileSelectionError(`Local attachment is not a regular file: ${filePath}`);
31908
32037
  if (fileStat.size === 0)
31909
32038
  throw new LocalFileSelectionError(`Local attachment is empty: ${filePath}`);
31910
- if (fileStat.size > maxFileBytes)
31911
- throw new LocalFileSelectionError(`Local attachment exceeds ${maxFileBytes} bytes: ${filePath}`);
31912
- const bytes = await fileHandle.readFile();
32039
+ if (fileStat.size > fileUploadMaxBytes)
32040
+ throw oversizedLocalFileError(filePath);
32041
+ const readResult = await readFileHandleWithLimit({
32042
+ fileHandle,
32043
+ initialByteSize: fileStat.size,
32044
+ maxBytes: fileUploadMaxBytes
32045
+ });
32046
+ if (readResult.status === "limit_exceeded")
32047
+ throw oversizedLocalFileError(filePath);
32048
+ const { bytes } = readResult;
31913
32049
  if (bytes.byteLength === 0)
31914
32050
  throw new LocalFileSelectionError(`Local attachment is empty: ${filePath}`);
31915
- if (bytes.byteLength > maxFileBytes)
31916
- throw new LocalFileSelectionError(`Local attachment exceeds ${maxFileBytes} bytes: ${filePath}`);
31917
32051
  return {
31918
32052
  filename: basename(filePath),
31919
32053
  mediaType,
@@ -31925,6 +32059,9 @@ async function readLocalUploadFile({ filePath, mediaType }) {
31925
32059
  await fileHandle.close();
31926
32060
  }
31927
32061
  }
32062
+ function oversizedLocalFileError(filePath) {
32063
+ return new LocalFileSelectionError(`Local attachment ${JSON.stringify(filePath)} is larger than ${fileUploadMaxSizeLabel}. Choose a file no larger than ${fileUploadMaxSizeLabel}.`);
32064
+ }
31928
32065
  async function parseJson(response, failureMessage) {
31929
32066
  try {
31930
32067
  return await response.json();
@@ -32675,11 +32812,17 @@ var sessionTelaPageArtifactEventSchema = exports_external2.strictObject({
32675
32812
  published_at: exports_external2.iso.datetime()
32676
32813
  });
32677
32814
  var sessionArtifactEventSchema = exports_external2.discriminatedUnion("kind", [
32678
- sessionFileArtifactEventSchema,
32815
+ sessionFileArtifactEventSchema.extend({
32816
+ download_url: exports_external2.string().optional(),
32817
+ preview_url: exports_external2.url().optional()
32818
+ }),
32679
32819
  sessionTelaPageArtifactEventSchema
32680
32820
  ]);
32681
32821
  var sessionArtifactSchema = exports_external2.discriminatedUnion("kind", [
32682
- sessionFileArtifactEventSchema.extend({ download_url: exports_external2.string() }),
32822
+ sessionFileArtifactEventSchema.extend({
32823
+ download_url: exports_external2.string(),
32824
+ preview_url: exports_external2.url().optional()
32825
+ }),
32683
32826
  sessionTelaPageArtifactEventSchema.extend({ preview_url: exports_external2.string() })
32684
32827
  ]);
32685
32828
  var sessionMessageSchema = exports_external2.strictObject({
@@ -32721,6 +32864,10 @@ var sessionConnectionSchema = exports_external2.discriminatedUnion("status", [
32721
32864
  var createCodexSessionInputSchema = exports_external2.strictObject({
32722
32865
  installationId: exports_external2.string().min(1),
32723
32866
  repositoryIds: exports_external2.array(exports_external2.string().min(1)),
32867
+ repositoryBranchOverrides: exports_external2.array(exports_external2.strictObject({
32868
+ repositoryId: exports_external2.string().min(1),
32869
+ branchName: exports_external2.string().min(1)
32870
+ })).optional(),
32724
32871
  prompt: exports_external2.string().refine((value) => value.trim().length > 0),
32725
32872
  fileIds: exports_external2.array(exports_external2.string().min(1)),
32726
32873
  model: codexModelIdSchema.optional(),
@@ -32762,6 +32909,7 @@ var withdrawSessionMessageResponseSchema = exports_external2.strictObject({
32762
32909
  });
32763
32910
  var sessionDetailResponseSchema = exports_external2.object({
32764
32911
  id: exports_external2.string(),
32912
+ title: exports_external2.string().nullable(),
32765
32913
  status: sessionStatusSchema,
32766
32914
  session_number: exports_external2.number().int().positive(),
32767
32915
  agent_status: exports_external2.string().min(1),
@@ -32852,7 +33000,13 @@ async function createCodexSession({
32852
33000
  ...parsedInput.reasoningEffort ? { reasoning_effort: parsedInput.reasoningEffort } : {}
32853
33001
  }
32854
33002
  } : {},
32855
- repositories: parsedInput.repositoryIds.map((repositoryId) => ({ repository_id: repositoryId })),
33003
+ repositories: parsedInput.repositoryIds.map((repositoryId) => {
33004
+ const override = parsedInput.repositoryBranchOverrides?.find((candidate) => candidate.repositoryId === repositoryId);
33005
+ return {
33006
+ repository_id: repositoryId,
33007
+ ...override ? { branch_name: override.branchName } : {}
33008
+ };
33009
+ }),
32856
33010
  message: {
32857
33011
  text: parsedInput.prompt,
32858
33012
  file_ids: parsedInput.fileIds
@@ -32934,6 +33088,8 @@ async function listSessions({
32934
33088
  client,
32935
33089
  limit,
32936
33090
  after,
33091
+ statuses,
33092
+ creatorUserIds,
32937
33093
  signal
32938
33094
  }) {
32939
33095
  const searchParams = new URLSearchParams;
@@ -32941,6 +33097,10 @@ async function listSessions({
32941
33097
  searchParams.set("limit", String(limit));
32942
33098
  if (after)
32943
33099
  searchParams.set("after", after);
33100
+ for (const status of statuses ?? [])
33101
+ searchParams.append("status", status);
33102
+ for (const creatorUserId of creatorUserIds ?? [])
33103
+ searchParams.append("creator_user_id", creatorUserId);
32944
33104
  const suffix = searchParams.size > 0 ? `?${searchParams.toString()}` : "";
32945
33105
  const response = await client.request(`/v1/sessions${suffix}`, { signal });
32946
33106
  const parsed = sessionListResponseSchema.safeParse(await parseJson2(response, "Session list response was not valid JSON."));
@@ -33027,7 +33187,8 @@ async function listSessionEvents({
33027
33187
  client,
33028
33188
  sessionId,
33029
33189
  limit,
33030
- after
33190
+ after,
33191
+ signal
33031
33192
  }) {
33032
33193
  const searchParams = new URLSearchParams;
33033
33194
  if (limit !== undefined)
@@ -33035,7 +33196,7 @@ async function listSessionEvents({
33035
33196
  if (after)
33036
33197
  searchParams.set("after", after);
33037
33198
  const suffix = searchParams.size > 0 ? `?${searchParams.toString()}` : "";
33038
- const response = await client.request(`/v1/sessions/${encodeURIComponent(sessionId)}/events${suffix}`);
33199
+ const response = await client.request(`/v1/sessions/${encodeURIComponent(sessionId)}/events${suffix}`, { signal });
33039
33200
  const parsed = sessionEventsResponseSchema.safeParse(await parseJson3(response, "Session events response was not valid JSON."));
33040
33201
  if (!parsed.success)
33041
33202
  throw new CodingAgentProtocolError("Session events response did not match the public API contract.", { cause: parsed.error });
@@ -33971,7 +34132,9 @@ function createSessionViewState({ detail, activeMessageId }) {
33971
34132
  kind: "session-view",
33972
34133
  sessionId: detail.id,
33973
34134
  ...detail.sessionNumber === undefined ? {} : { sessionNumber: detail.sessionNumber },
34135
+ title: detail.title ?? null,
33974
34136
  aggregateStatus: detail.status,
34137
+ ...detail.agentStatus === undefined ? {} : { agentStatus: detail.agentStatus },
33975
34138
  connectionStatus: "connected",
33976
34139
  pullRequest: toSessionPullRequest(detail),
33977
34140
  ...activeMessageId !== undefined ? { activeMessageId } : {},
@@ -34004,7 +34167,9 @@ function updateSessionDetail({ state, detail }) {
34004
34167
  ...state,
34005
34168
  sessionId: detail.id,
34006
34169
  ...detail.sessionNumber === undefined ? {} : { sessionNumber: detail.sessionNumber },
34170
+ ..."title" in detail ? { title: detail.title ?? null } : {},
34007
34171
  aggregateStatus: detail.status,
34172
+ ...detail.agentStatus === undefined ? {} : { agentStatus: detail.agentStatus },
34008
34173
  pullRequest: toSessionPullRequest(detail),
34009
34174
  connectionPreviews: toConnectionPreviews(detail)
34010
34175
  };
@@ -34076,7 +34241,9 @@ function projectRetainedEvent({
34076
34241
  artifactId: artifact.artifact_id,
34077
34242
  occurredAt,
34078
34243
  title: artifact.filename,
34079
- mediaType: artifact.media_type
34244
+ mediaType: artifact.media_type,
34245
+ ...artifact.download_url === undefined ? {} : { downloadUrl: artifact.download_url },
34246
+ ...artifact.preview_url === undefined ? {} : { previewUrl: artifact.preview_url }
34080
34247
  } : {
34081
34248
  kind: "artifact",
34082
34249
  artifactKind: artifact.kind,
@@ -35061,12 +35228,12 @@ function createRemoteSessionController(dependencies) {
35061
35228
  detail: input.detail,
35062
35229
  activeMessageId: dependencies.activeMessageId ?? cache?.activeMessageId
35063
35230
  });
35064
- resolveReady();
35065
- publishState();
35066
35231
  if (input.mode === "cold-resume") {
35067
35232
  const hydrationGeneration = beginReasoningAttempt();
35068
35233
  await hydrateRetainedHistoryFromBeginning(hydrationGeneration);
35069
35234
  }
35235
+ resolveReady();
35236
+ publishState();
35070
35237
  await streamWithControllerReconnects();
35071
35238
  }
35072
35239
  function stop() {
@@ -35102,7 +35269,8 @@ function createRemoteSessionController(dependencies) {
35102
35269
  async function recordActiveMessage(sessionMessageId) {
35103
35270
  state = {
35104
35271
  ...getState(),
35105
- activeMessageId: sessionMessageId
35272
+ activeMessageId: sessionMessageId,
35273
+ agentStatus: undefined
35106
35274
  };
35107
35275
  await writeCache();
35108
35276
  publishState();
@@ -35123,7 +35291,8 @@ function createRemoteSessionController(dependencies) {
35123
35291
  const page = await dependencies.listSessionEvents({
35124
35292
  sessionId: dependencies.sessionId,
35125
35293
  limit: sessionHistoryPageSize,
35126
- after
35294
+ after,
35295
+ signal: abortController.signal
35127
35296
  });
35128
35297
  for (const item of page.data) {
35129
35298
  await reduceFrameAndPersist({
@@ -35377,7 +35546,10 @@ async function sleepMs(ms, signal) {
35377
35546
  }
35378
35547
 
35379
35548
  // src/tui/dashboard.ts
35380
- import { bg, BoxRenderable, bold as bold2, CliRenderEvents as CliRenderEvents2, fg as fg3, ScrollBoxRenderable, stringToStyledText as stringToStyledText2, TextRenderable } from "@opentui/core";
35549
+ import { bg, BoxRenderable as BoxRenderable2, bold as bold2, CliRenderEvents as CliRenderEvents2, fg as fg3, ScrollBoxRenderable, stringToStyledText as stringToStyledText2, TextRenderable as TextRenderable2 } from "@opentui/core";
35550
+
35551
+ // src/tui/composer.ts
35552
+ import { BoxRenderable, TextRenderable, TextareaRenderable } from "@opentui/core";
35381
35553
 
35382
35554
  // src/tui/composer-divider.ts
35383
35555
  import { dim as dim2, fg as fg2, StyledText as StyledText2 } from "@opentui/core";
@@ -35572,6 +35744,98 @@ function renderComposerDivider({ width, tag, leadLabel }) {
35572
35744
  ]);
35573
35745
  }
35574
35746
 
35747
+ // src/tui/composer-height.ts
35748
+ var composerMaxRows = 6;
35749
+ function resizeComposer(composer) {
35750
+ composer.height = Math.min(Math.max(composer.editorView.getTotalVirtualLineCount(), 1), composerMaxRows);
35751
+ }
35752
+
35753
+ // src/tui/composer.ts
35754
+ var defaultPlaceholder = "Message Remy\u2026 Tab paths attach \xB7 Enter/Shift+Enter send \xB7 Option+Enter newline";
35755
+ function createComposer({
35756
+ renderer,
35757
+ parent,
35758
+ tag,
35759
+ topDividerLabel,
35760
+ placeholder = defaultPlaceholder,
35761
+ screenPaddingX = 1,
35762
+ onSubmit,
35763
+ onContentChange
35764
+ }) {
35765
+ const root = new BoxRenderable(renderer, { width: "100%", flexDirection: "column", flexShrink: 0 });
35766
+ const dividerTop = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35767
+ const textarea = new TextareaRenderable(renderer, {
35768
+ width: "100%",
35769
+ height: 1,
35770
+ flexShrink: 0,
35771
+ wrapMode: "word",
35772
+ placeholder,
35773
+ keyBindings: [
35774
+ { name: "return", action: "submit" },
35775
+ { name: "return", shift: true, action: "submit" },
35776
+ { name: "return", meta: true, action: "newline" },
35777
+ { name: "j", ctrl: true, action: "newline" }
35778
+ ],
35779
+ onSubmit
35780
+ });
35781
+ const dividerBottom = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35782
+ let mounted = false;
35783
+ root.add(dividerTop);
35784
+ root.add(textarea);
35785
+ root.add(dividerBottom);
35786
+ textarea.onContentChange = () => {
35787
+ resizeComposer(textarea);
35788
+ onContentChange?.();
35789
+ };
35790
+ function render() {
35791
+ const dividerWidth = renderer.width - screenPaddingX * 2;
35792
+ dividerTop.content = renderComposerDivider({ width: dividerWidth, tag, leadLabel: topDividerLabel?.() });
35793
+ dividerBottom.content = renderComposerDivider({ width: dividerWidth });
35794
+ }
35795
+ return {
35796
+ textarea,
35797
+ mount() {
35798
+ if (mounted)
35799
+ return;
35800
+ mounted = true;
35801
+ parent.add(root);
35802
+ resizeComposer(textarea);
35803
+ render();
35804
+ },
35805
+ unmount() {
35806
+ if (!mounted)
35807
+ return;
35808
+ mounted = false;
35809
+ parent.remove(root);
35810
+ },
35811
+ render,
35812
+ focus() {
35813
+ textarea.focus();
35814
+ },
35815
+ destroy() {
35816
+ if (mounted)
35817
+ parent.remove(root);
35818
+ mounted = false;
35819
+ root.destroyRecursively();
35820
+ }
35821
+ };
35822
+ }
35823
+
35824
+ // src/tui/fuzzy-match.ts
35825
+ function fuzzyMatches(value, query) {
35826
+ const normalizedQuery = query.replaceAll(/\s/g, "").toLowerCase();
35827
+ if (!normalizedQuery)
35828
+ return true;
35829
+ let queryIndex = 0;
35830
+ for (const character of value.toLowerCase()) {
35831
+ if (character === normalizedQuery[queryIndex])
35832
+ queryIndex += 1;
35833
+ if (queryIndex === normalizedQuery.length)
35834
+ return true;
35835
+ }
35836
+ return false;
35837
+ }
35838
+
35575
35839
  // src/tui/renderer.ts
35576
35840
  import { CliRenderEvents, createClipboard, createCliRenderer, createHostClipboard, createRendererClipboardAdapter } from "@opentui/core";
35577
35841
  var rendererDestroyed = new WeakMap;
@@ -35654,8 +35918,17 @@ async function renderRemyView({
35654
35918
 
35655
35919
  // src/tui/dashboard.ts
35656
35920
  var DASHBOARD_POLL_INTERVAL_MS = 1e4;
35921
+ var lifecycleStatusOptions = [
35922
+ { value: "open", label: "Open" },
35923
+ { value: "completed", label: "Completed" },
35924
+ { value: "failed", label: "Failed" },
35925
+ { value: "cancelled", label: "Cancelled" }
35926
+ ];
35657
35927
  async function createDashboardTui({
35658
35928
  initialPage,
35929
+ initialFilters = { statuses: [], creators: [] },
35930
+ authors: initialAuthors = [],
35931
+ loadAuthors,
35659
35932
  loadPage,
35660
35933
  createRenderer = createDefaultRenderer
35661
35934
  }) {
@@ -35664,6 +35937,7 @@ async function createDashboardTui({
35664
35937
  let keyHandler;
35665
35938
  let resizeHandler;
35666
35939
  let refreshTimer;
35940
+ let contentScrollHandler;
35667
35941
  let rendererDestroyed2 = false;
35668
35942
  const loadPageAbortController = new AbortController;
35669
35943
  try {
@@ -35675,28 +35949,61 @@ async function createDashboardTui({
35675
35949
  loadPageAbortController.abort();
35676
35950
  }, render = function() {
35677
35951
  const contentWidth = renderer.width - 4;
35678
- const rowsBelow = rowsBelowVisibleWindow({ sessionCount: page.sessions.length, selectedIndex, height: renderer.height });
35952
+ const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
35953
+ const rowsBelow = rowsBelowVisibleWindow({ sessionCount: sessions.length, selectedIndex, height: renderer.height });
35679
35954
  const isNarrow = renderer.width < 72;
35680
- const selectedSession = page.sessions[selectedIndex];
35681
- const sessionRows = page.sessions.length === 0 ? stringToStyledText2("No remote sessions yet.") : isNarrow ? formatSessionCard({
35955
+ const selectedSession = filterMode ? undefined : sessions[selectedIndex];
35956
+ 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({
35682
35957
  session: selectedSession,
35683
35958
  selectedIndex,
35684
- sessionCount: page.sessions.length,
35959
+ sessionCount: sessions.length,
35685
35960
  width: contentWidth
35686
35961
  }) : formatSessionList({
35687
- sessions: page.sessions,
35962
+ sessions,
35688
35963
  selectedIndex,
35689
35964
  width: contentWidth,
35690
35965
  height: renderer.height
35691
35966
  });
35692
- statusBand.content = dashboardStatusBand({ page, pageIndex });
35967
+ statusBand.content = dashboardStatusBand({ page, pageIndex, filters, sessionSearchQuery });
35693
35968
  content.content = joinStyled([stringToStyledText2(""), sessionRows], `
35694
35969
  `);
35970
+ desiredContentScrollY = filterMode ? filterMode.selectedIndex + 2 : 0;
35971
+ if (!contentScrollHandler) {
35972
+ contentScrollHandler = () => {
35973
+ contentScrollHandler = undefined;
35974
+ if (destroyed)
35975
+ return;
35976
+ listRegion.scrollTo({ x: 0, y: desiredContentScrollY });
35977
+ renderer.requestRender();
35978
+ };
35979
+ renderer.once(CliRenderEvents2.FRAME, contentScrollHandler);
35980
+ }
35695
35981
  const showPreview = !isNarrow && selectedSession !== undefined;
35696
35982
  previewBand.visible = showPreview;
35697
35983
  previewBand.content = showPreview ? formatSessionPreview({ session: selectedSession, width: contentWidth }) : stringToStyledText2("");
35698
- separator.content = renderComposerDivider({ width: contentWidth });
35699
- footer.content = dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen });
35984
+ const activeSearchComposer = filterMode?.searching ? "filter" : sessionSearchOpen ? "session" : undefined;
35985
+ if (activeSearchComposer !== mountedSearchComposer) {
35986
+ if (mountedSearchComposer === "filter")
35987
+ filterSearchComposer.unmount();
35988
+ else if (mountedSearchComposer === "session")
35989
+ sessionSearchComposer.unmount();
35990
+ mountedSearchComposer = activeSearchComposer;
35991
+ if (activeSearchComposer === "filter") {
35992
+ filterSearchComposer.mount();
35993
+ filterSearchComposer.focus();
35994
+ } else if (activeSearchComposer === "session") {
35995
+ sessionSearchComposer.mount();
35996
+ sessionSearchComposer.focus();
35997
+ } else {
35998
+ listRegion.focus();
35999
+ }
36000
+ }
36001
+ if (activeSearchComposer === "filter")
36002
+ filterSearchComposer.render();
36003
+ else if (activeSearchComposer === "session")
36004
+ sessionSearchComposer.render();
36005
+ separator.content = activeSearchComposer ? "" : renderComposerDivider({ width: contentWidth });
36006
+ footer.content = filterMode ? dashboardFilterFooter({ mode: filterMode, loadingPage, loadingAuthors, pageError, authorError }) : dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen, sessionSearchOpen });
35700
36007
  renderer.requestRender();
35701
36008
  }, finish = function(value) {
35702
36009
  if (settled)
@@ -35711,9 +36018,40 @@ async function createDashboardTui({
35711
36018
  stopPageRefresh();
35712
36019
  rejectDraft(error93);
35713
36020
  }, moveSelectionTo = function(index) {
35714
- if (page.sessions.length === 0)
36021
+ const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
36022
+ if (sessions.length === 0)
36023
+ return;
36024
+ selectedIndex = Math.max(0, Math.min(index, sessions.length - 1));
36025
+ render();
36026
+ }, applyFilterSearch = function() {
36027
+ if (!filterMode?.searching)
36028
+ return;
36029
+ filterMode = { ...filterMode, searching: false, selectedIndex: 0 };
36030
+ filterSearchComposer.textarea.setText("");
36031
+ render();
36032
+ }, cancelFilterSearch = function() {
36033
+ if (!filterMode?.searching)
36034
+ return;
36035
+ filterMode = { ...filterMode, searching: false, query: "", selectedIndex: 0 };
36036
+ filterSearchComposer.textarea.setText("");
36037
+ render();
36038
+ }, openSessionSearch = function() {
36039
+ sessionSearchOpen = true;
36040
+ sessionSearchComposer.textarea.setText(sessionSearchQuery);
36041
+ render();
36042
+ }, applySessionSearch = function() {
36043
+ if (!sessionSearchOpen)
36044
+ return;
36045
+ sessionSearchOpen = false;
36046
+ selectedIndex = 0;
36047
+ render();
36048
+ }, cancelSessionSearch = function() {
36049
+ if (!sessionSearchOpen)
35715
36050
  return;
35716
- selectedIndex = Math.max(0, Math.min(index, page.sessions.length - 1));
36051
+ sessionSearchOpen = false;
36052
+ sessionSearchQuery = "";
36053
+ selectedIndex = 0;
36054
+ sessionSearchComposer.textarea.setText("");
35717
36055
  render();
35718
36056
  }, handleSessionKey = function(key) {
35719
36057
  if (logoutConfirmationOpen) {
@@ -35726,6 +36064,84 @@ async function createDashboardTui({
35726
36064
  }
35727
36065
  return;
35728
36066
  }
36067
+ if (sessionSearchOpen) {
36068
+ if (key.name === "escape") {
36069
+ key.preventDefault();
36070
+ cancelSessionSearch();
36071
+ }
36072
+ return;
36073
+ }
36074
+ if (filterMode) {
36075
+ const options = visibleDashboardFilterOptions({ mode: filterMode, authors });
36076
+ if (key.name === "escape") {
36077
+ key.preventDefault();
36078
+ if (filterMode.searching) {
36079
+ cancelFilterSearch();
36080
+ return;
36081
+ }
36082
+ filterMode = undefined;
36083
+ pageError = undefined;
36084
+ authorError = undefined;
36085
+ render();
36086
+ return;
36087
+ }
36088
+ if (filterMode.searching)
36089
+ return;
36090
+ if (loadingAuthors) {
36091
+ key.preventDefault();
36092
+ return;
36093
+ }
36094
+ const filterListFocused = renderer.currentFocusedRenderable === listRegion;
36095
+ if (filterListFocused && (key.name === "G" || key.name === "g" && key.shift)) {
36096
+ key.preventDefault();
36097
+ awaitingVimGoToTop = false;
36098
+ if (options.length > 0)
36099
+ filterMode = { ...filterMode, selectedIndex: options.length - 1 };
36100
+ render();
36101
+ return;
36102
+ }
36103
+ if (filterListFocused && key.name === "g") {
36104
+ key.preventDefault();
36105
+ if (awaitingVimGoToTop && options.length > 0)
36106
+ filterMode = { ...filterMode, selectedIndex: 0 };
36107
+ awaitingVimGoToTop = !awaitingVimGoToTop;
36108
+ render();
36109
+ return;
36110
+ }
36111
+ awaitingVimGoToTop = false;
36112
+ if (key.name === "down" || key.name === "up" || key.name === "j" || key.name === "k") {
36113
+ key.preventDefault();
36114
+ if (options.length > 0) {
36115
+ const backwards = key.name === "up" || key.name === "k";
36116
+ filterMode = { ...filterMode, selectedIndex: (filterMode.selectedIndex + (backwards ? -1 : 1) + options.length) % options.length };
36117
+ }
36118
+ render();
36119
+ return;
36120
+ }
36121
+ if (key.name === "space" || key.name === "left" || key.name === "right" || key.name === "h" || key.name === "l") {
36122
+ key.preventDefault();
36123
+ const option = options[filterMode.selectedIndex];
36124
+ if (!option)
36125
+ return;
36126
+ const selected = key.name === "space" ? !isDashboardFilterOptionSelected({ mode: filterMode, option }) : key.name === "right" || key.name === "l";
36127
+ filterMode = updateDashboardFilterSelection({ mode: filterMode, option, selected, authors });
36128
+ render();
36129
+ return;
36130
+ }
36131
+ if (key.name === "s") {
36132
+ key.preventDefault();
36133
+ filterMode = { ...filterMode, query: "", searching: true, selectedIndex: 0 };
36134
+ filterSearchComposer.textarea.setText("");
36135
+ render();
36136
+ return;
36137
+ }
36138
+ if (key.name === "return" || key.name === "enter") {
36139
+ key.preventDefault();
36140
+ const nextFilters = { statuses: [...filterMode.statuses], creators: [...filterMode.creators] };
36141
+ applyFilters(nextFilters);
36142
+ }
36143
+ return;
36144
+ }
35729
36145
  const listFocused = renderer.currentFocusedRenderable === listRegion;
35730
36146
  if (key.name === "down") {
35731
36147
  key.preventDefault();
@@ -35754,7 +36170,7 @@ async function createDashboardTui({
35754
36170
  if (listFocused && (key.name === "G" || key.name === "g" && key.shift)) {
35755
36171
  key.preventDefault();
35756
36172
  awaitingVimGoToTop = false;
35757
- moveSelectionTo(page.sessions.length - 1);
36173
+ moveSelectionTo(visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery }).length - 1);
35758
36174
  return;
35759
36175
  }
35760
36176
  if (listFocused && key.name === "g") {
@@ -35779,7 +36195,7 @@ async function createDashboardTui({
35779
36195
  awaitingVimGoToTop = false;
35780
36196
  if (key.name === "return" || key.name === "enter") {
35781
36197
  key.preventDefault();
35782
- const session = page.sessions[selectedIndex];
36198
+ const session = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery })[selectedIndex];
35783
36199
  if (session)
35784
36200
  finish({ kind: "session", sessionId: session.id });
35785
36201
  return;
@@ -35789,6 +36205,23 @@ async function createDashboardTui({
35789
36205
  finish({ kind: "new" });
35790
36206
  return;
35791
36207
  }
36208
+ if (key.name === "s") {
36209
+ key.preventDefault();
36210
+ openSessionSearch();
36211
+ return;
36212
+ }
36213
+ if (key.name === "f") {
36214
+ key.preventDefault();
36215
+ authorError = undefined;
36216
+ filterMode = createFilterMode({ kind: "status", filters });
36217
+ render();
36218
+ return;
36219
+ }
36220
+ if (key.name === "a") {
36221
+ key.preventDefault();
36222
+ openAuthorFilter();
36223
+ return;
36224
+ }
35792
36225
  if (key.name === "L" || key.shift && key.name === "l") {
35793
36226
  key.preventDefault();
35794
36227
  logoutConfirmationOpen = true;
@@ -35805,21 +36238,19 @@ async function createDashboardTui({
35805
36238
  finish(null);
35806
36239
  }
35807
36240
  };
35808
- const root = new BoxRenderable(renderer, { width: "100%", height: "100%", flexDirection: "column", paddingX: 2, overflow: "hidden" });
35809
- const statusBand = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35810
- const content = new TextRenderable(renderer, { content: "" });
36241
+ const root = new BoxRenderable2(renderer, { width: "100%", height: "100%", flexDirection: "column", paddingX: 2, overflow: "hidden" });
36242
+ const statusBand = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
36243
+ const content = new TextRenderable2(renderer, { content: "" });
35811
36244
  const listRegion = new ScrollBoxRenderable(renderer, { flexGrow: 1, flexShrink: 1, minHeight: 0, scrollY: true });
35812
- const previewBand = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35813
- const separator = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35814
- const footer = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35815
- root.onMouseDown = (event) => {
35816
- event.preventDefault();
35817
- listRegion.focus();
35818
- };
36245
+ const previewBand = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
36246
+ const filterSearchRegion = new BoxRenderable2(renderer, { width: "100%", flexShrink: 0 });
36247
+ const separator = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
36248
+ const footer = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
35819
36249
  listRegion.add(content);
35820
36250
  root.add(statusBand);
35821
36251
  root.add(listRegion);
35822
36252
  root.add(previewBand);
36253
+ root.add(filterSearchRegion);
35823
36254
  root.add(separator);
35824
36255
  root.add(footer);
35825
36256
  renderer.root.add(root);
@@ -35829,8 +36260,22 @@ async function createDashboardTui({
35829
36260
  let loadingPage = false;
35830
36261
  let pageRequestInFlight = false;
35831
36262
  let pageError;
36263
+ let authorError;
36264
+ let authors = initialAuthors;
36265
+ let authorsLoaded = loadAuthors === undefined || initialAuthors.length > 0;
36266
+ let loadingAuthors = false;
35832
36267
  let awaitingVimGoToTop = false;
35833
36268
  let logoutConfirmationOpen = false;
36269
+ let filters = initialFilters;
36270
+ let filterMode;
36271
+ let sessionSearchOpen = false;
36272
+ let sessionSearchQuery = "";
36273
+ root.onMouseDown = (event) => {
36274
+ if (filterMode?.searching || sessionSearchOpen)
36275
+ return;
36276
+ event.preventDefault();
36277
+ listRegion.focus();
36278
+ };
35834
36279
  let destroyed = false;
35835
36280
  let rendererDestroyPromise;
35836
36281
  let settled = false;
@@ -35840,10 +36285,41 @@ async function createDashboardTui({
35840
36285
  let rejectDraft = () => {
35841
36286
  return;
35842
36287
  };
36288
+ let desiredContentScrollY = 0;
35843
36289
  const action = new Promise((resolve, reject) => {
35844
36290
  resolveAction = resolve;
35845
36291
  rejectDraft = reject;
35846
36292
  });
36293
+ let mountedSearchComposer;
36294
+ const filterSearchComposer = createComposer({
36295
+ renderer,
36296
+ parent: filterSearchRegion,
36297
+ tag: "Filter",
36298
+ placeholder: "Filter options\u2026",
36299
+ screenPaddingX: 2,
36300
+ onSubmit: () => applyFilterSearch(),
36301
+ onContentChange: () => {
36302
+ if (!filterMode?.searching)
36303
+ return;
36304
+ filterMode = { ...filterMode, query: filterSearchComposer.textarea.plainText, selectedIndex: 0 };
36305
+ render();
36306
+ }
36307
+ });
36308
+ const sessionSearchComposer = createComposer({
36309
+ renderer,
36310
+ parent: filterSearchRegion,
36311
+ tag: "Search",
36312
+ placeholder: "Search by ID, title, or number\u2026",
36313
+ screenPaddingX: 2,
36314
+ onSubmit: () => applySessionSearch(),
36315
+ onContentChange: () => {
36316
+ if (!sessionSearchOpen)
36317
+ return;
36318
+ sessionSearchQuery = sessionSearchComposer.textarea.plainText;
36319
+ selectedIndex = 0;
36320
+ render();
36321
+ }
36322
+ });
35847
36323
  async function loadAdjacentPage(direction) {
35848
36324
  if (pageRequestInFlight)
35849
36325
  return;
@@ -35860,7 +36336,8 @@ async function createDashboardTui({
35860
36336
  return;
35861
36337
  page = loadedPage;
35862
36338
  pageIndex = Math.max(1, pageIndex + (direction === "next" ? 1 : -1));
35863
- selectedIndex = direction === "next" ? 0 : Math.max(0, page.sessions.length - 1);
36339
+ const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
36340
+ selectedIndex = direction === "next" ? 0 : Math.max(0, sessions.length - 1);
35864
36341
  } catch (error93) {
35865
36342
  if (!destroyed && !settled)
35866
36343
  pageError = error93 instanceof Error ? error93.message : String(error93);
@@ -35879,7 +36356,7 @@ async function createDashboardTui({
35879
36356
  const refreshedPage = await loadPage({ target: "current", signal: loadPageAbortController.signal });
35880
36357
  if (destroyed || settled)
35881
36358
  return;
35882
- const selectedSessionId = page.sessions[selectedIndex]?.id;
36359
+ const selectedSessionId = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery })[selectedIndex]?.id;
35883
36360
  page = refreshedPage;
35884
36361
  const refreshedSelectedIndex = selectedSessionId ? page.sessions.findIndex((session) => session.id === selectedSessionId) : -1;
35885
36362
  selectedIndex = refreshedSelectedIndex >= 0 ? refreshedSelectedIndex : Math.max(0, Math.min(selectedIndex, page.sessions.length - 1));
@@ -35894,10 +36371,53 @@ async function createDashboardTui({
35894
36371
  pageRequestInFlight = false;
35895
36372
  }
35896
36373
  }
36374
+ async function applyFilters(nextFilters) {
36375
+ if (loadingPage)
36376
+ return;
36377
+ loadingPage = true;
36378
+ pageError = undefined;
36379
+ authorError = undefined;
36380
+ render();
36381
+ try {
36382
+ page = await loadPage({ filters: nextFilters, signal: loadPageAbortController.signal });
36383
+ filters = nextFilters;
36384
+ pageIndex = 1;
36385
+ selectedIndex = 0;
36386
+ filterMode = undefined;
36387
+ } catch (error93) {
36388
+ pageError = error93 instanceof Error ? error93.message : String(error93);
36389
+ } finally {
36390
+ loadingPage = false;
36391
+ render();
36392
+ }
36393
+ }
36394
+ async function openAuthorFilter() {
36395
+ filterMode = createFilterMode({ kind: "creator", filters });
36396
+ authorError = undefined;
36397
+ render();
36398
+ if (authorsLoaded || loadingAuthors || !loadAuthors)
36399
+ return;
36400
+ loadingAuthors = true;
36401
+ render();
36402
+ try {
36403
+ authors = await loadAuthors();
36404
+ authorsLoaded = true;
36405
+ if (filterMode?.kind === "creator") {
36406
+ const firstSelectedIndex = authors.findIndex((author) => filterMode?.creators.some((creator) => creator.id === author.id));
36407
+ filterMode = { ...filterMode, selectedIndex: Math.max(0, firstSelectedIndex) };
36408
+ }
36409
+ } catch (error93) {
36410
+ authorError = error93 instanceof Error ? error93.message : String(error93);
36411
+ } finally {
36412
+ loadingAuthors = false;
36413
+ render();
36414
+ }
36415
+ }
35897
36416
  async function moveSelection(direction) {
35898
- if (loadingPage || page.sessions.length === 0)
36417
+ const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
36418
+ if (loadingPage || sessions.length === 0)
35899
36419
  return;
35900
- const isBoundary = direction === "next" ? selectedIndex === page.sessions.length - 1 : selectedIndex === 0;
36420
+ const isBoundary = direction === "next" ? selectedIndex === sessions.length - 1 : selectedIndex === 0;
35901
36421
  if (!isBoundary) {
35902
36422
  selectedIndex += direction === "next" ? 1 : -1;
35903
36423
  render();
@@ -35912,7 +36432,13 @@ async function createDashboardTui({
35912
36432
  }
35913
36433
  return false;
35914
36434
  };
35915
- keyHandler = handleSessionKey;
36435
+ filterSearchComposer.textarea.onKeyDown = handleSessionKey;
36436
+ sessionSearchComposer.textarea.onKeyDown = handleSessionKey;
36437
+ keyHandler = (key) => {
36438
+ if (mountedSearchComposer && key.name !== "escape" && !(key.ctrl && key.name === "c"))
36439
+ return;
36440
+ handleSessionKey(key);
36441
+ };
35916
36442
  renderer.addInputHandler(inputHandler);
35917
36443
  renderer.keyInput.on("keypress", keyHandler);
35918
36444
  renderer.on(CliRenderEvents2.RENDER_ERROR, (event) => fail(event.error));
@@ -35939,6 +36465,10 @@ async function createDashboardTui({
35939
36465
  if (resizeHandler)
35940
36466
  renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
35941
36467
  stopPageRefresh();
36468
+ if (contentScrollHandler)
36469
+ renderer.off(CliRenderEvents2.FRAME, contentScrollHandler);
36470
+ filterSearchComposer.destroy();
36471
+ sessionSearchComposer.destroy();
35942
36472
  rendererDestroyed2 = true;
35943
36473
  renderer.destroy();
35944
36474
  },
@@ -35956,6 +36486,8 @@ async function createDashboardTui({
35956
36486
  if (refreshTimer !== undefined)
35957
36487
  clearInterval(refreshTimer);
35958
36488
  loadPageAbortController.abort();
36489
+ if (contentScrollHandler)
36490
+ renderer.off(CliRenderEvents2.FRAME, contentScrollHandler);
35959
36491
  if (!rendererDestroyed2) {
35960
36492
  rendererDestroyed2 = true;
35961
36493
  renderer.destroy();
@@ -35993,6 +36525,63 @@ function rowsBelowVisibleWindow({ sessionCount, selectedIndex, height }) {
35993
36525
  const { firstIndex, rowCount } = visibleListWindow({ sessionCount, selectedIndex, height });
35994
36526
  return Math.max(0, sessionCount - (firstIndex + rowCount));
35995
36527
  }
36528
+ function createFilterMode({ kind, filters }) {
36529
+ return {
36530
+ kind,
36531
+ selectedIndex: 0,
36532
+ statuses: [...filters.statuses],
36533
+ creators: [...filters.creators],
36534
+ query: "",
36535
+ searching: false
36536
+ };
36537
+ }
36538
+ function formatFilterMenu({ mode, authors, width }) {
36539
+ const options = visibleDashboardFilterOptions({ mode, authors });
36540
+ const selectedCount = mode.kind === "status" ? mode.statuses.length : mode.creators.length;
36541
+ const heading = `${mode.kind === "status" ? "Filter by status" : "Filter by author"} \xB7 ${selectedCount} selected \xB7 empty shows all`;
36542
+ const rows = options.map((option, index) => {
36543
+ const active = isDashboardFilterOptionSelected({ mode, option });
36544
+ const label = `${active ? "[x]" : "[ ]"} ${truncate(option.label, Math.max(1, width - 4))}`;
36545
+ const chunk = index === mode.selectedIndex ? bg(PALETTE.selectionBg)(fg3(PALETTE.bodyText)(label.padEnd(width))) : active ? bold2(fg3(PALETTE.humanAccent)(label)) : fg3(PALETTE.dimText)(label);
36546
+ return [chunk];
36547
+ });
36548
+ return joinStyled([
36549
+ [bold2(fg3(PALETTE.bodyText)(truncate(heading, width)))],
36550
+ ...rows.length > 0 ? rows : [[fg3(PALETTE.dimText)("No matching options.")]]
36551
+ ], `
36552
+ `);
36553
+ }
36554
+ function dashboardFilterOptions({ kind, authors }) {
36555
+ 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 }));
36556
+ }
36557
+ function visibleDashboardFilterOptions({ mode, authors }) {
36558
+ return dashboardFilterOptions({ kind: mode.kind, authors }).filter((option) => fuzzyMatches(option.label, mode.query));
36559
+ }
36560
+ function isDashboardFilterOptionSelected({ mode, option }) {
36561
+ return option.kind === "status" ? mode.statuses.includes(option.id) : mode.creators.some((creator) => creator.id === option.id);
36562
+ }
36563
+ function updateDashboardFilterSelection({ mode, option, selected, authors }) {
36564
+ if (option.kind === "status") {
36565
+ const selectedStatuses = new Set(mode.statuses);
36566
+ if (selected)
36567
+ selectedStatuses.add(option.id);
36568
+ else
36569
+ selectedStatuses.delete(option.id);
36570
+ return {
36571
+ ...mode,
36572
+ statuses: lifecycleStatusOptions.map((entry) => entry.value).filter((status) => selectedStatuses.has(status))
36573
+ };
36574
+ }
36575
+ const selectedCreatorIds = new Set(mode.creators.map((creator) => creator.id));
36576
+ if (selected)
36577
+ selectedCreatorIds.add(option.id);
36578
+ else
36579
+ selectedCreatorIds.delete(option.id);
36580
+ return {
36581
+ ...mode,
36582
+ creators: authors.filter((author) => selectedCreatorIds.has(author.id))
36583
+ };
36584
+ }
35996
36585
  function formatSessionList({ sessions, selectedIndex, width, height }) {
35997
36586
  const { firstIndex, rowCount } = visibleListWindow({ sessionCount: sessions.length, selectedIndex, height });
35998
36587
  const visibleSessions = sessions.slice(firstIndex, firstIndex + rowCount);
@@ -36079,18 +36668,34 @@ function formatUpdatedAt(value) {
36079
36668
  return `${Math.floor(seconds / (60 * 60))}h ago`;
36080
36669
  return new Date(milliseconds).toLocaleDateString(undefined, { month: "short", day: "numeric" });
36081
36670
  }
36082
- function dashboardStatusBand({ page, pageIndex }) {
36083
- const openCount = page.sessions.filter((session) => session.status === "open").length;
36671
+ function matchesSessionSearch({ session, query }) {
36672
+ return fuzzyMatches(session.id, query) || fuzzyMatches(session.title ?? "", query) || fuzzyMatches(String(session.sessionNumber), query);
36673
+ }
36674
+ function visibleDashboardSessions({ sessions, query }) {
36675
+ return sessions.filter((session) => matchesSessionSearch({ session, query }));
36676
+ }
36677
+ function dashboardStatusBand({ page, pageIndex, filters, sessionSearchQuery }) {
36678
+ const openCount = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery }).filter((session) => session.status === "open").length;
36084
36679
  const paging = [`page ${pageIndex}`];
36085
36680
  if (page.canGoPrevious)
36086
36681
  paging.push("\u2190 prev");
36087
36682
  if (page.hasMore)
36088
36683
  paging.push("more \u2192");
36089
- return `remy \xB7 sessions \xB7 ${openCount} open \xB7 ${paging.join(" \xB7 ")}`;
36684
+ const activeFilters = [
36685
+ filters.statuses.length > 0 ? `status: ${filters.statuses.map(sessionStatusLabel).join(", ")}` : undefined,
36686
+ filters.creators.length > 0 ? `author: ${filters.creators.map((creator) => creator.name).join(", ")}` : undefined,
36687
+ sessionSearchQuery ? `search: ${sessionSearchQuery}` : undefined
36688
+ ].filter((value) => value !== undefined);
36689
+ return `remy \xB7 sessions \xB7 ${openCount} open${activeFilters.length > 0 ? ` \xB7 ${activeFilters.join(" \xB7 ")}` : ""} \xB7 ${paging.join(" \xB7 ")}`;
36090
36690
  }
36091
- function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen }) {
36691
+ function hasActiveFilters(filters) {
36692
+ return filters.statuses.length > 0 || filters.creators.length > 0;
36693
+ }
36694
+ function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen, sessionSearchOpen }) {
36092
36695
  if (logoutConfirmationOpen)
36093
36696
  return "Log out of Remy? y confirms \xB7 Esc cancels";
36697
+ if (sessionSearchOpen)
36698
+ return "type to search \xB7 \u23CE apply search \xB7 Esc clear search";
36094
36699
  const parts = [];
36095
36700
  if (loadingPage)
36096
36701
  parts.push("Loading sessions\u2026");
@@ -36098,9 +36703,22 @@ function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmation
36098
36703
  parts.push(`Could not load sessions: ${pageError}`);
36099
36704
  if (rowsBelow > 0)
36100
36705
  parts.push(`\u2193 ${rowsBelow} more below`);
36101
- parts.push("\u2191\u2193 move \xB7 \u23CE open \xB7 n new session \xB7 \u21E7l log out \xB7 q quit");
36706
+ parts.push("\u2191\u2193 move \xB7 \u23CE open \xB7 s search \xB7 f status \xB7 a author \xB7 n new session \xB7 \u21E7l log out \xB7 q quit");
36102
36707
  return parts.join(" \xB7 ");
36103
36708
  }
36709
+ function dashboardFilterFooter({ mode, loadingPage, loadingAuthors, pageError, authorError }) {
36710
+ if (loadingAuthors)
36711
+ return "Loading authors\u2026 \xB7 Esc cancel";
36712
+ if (loadingPage)
36713
+ return "Applying filter\u2026";
36714
+ if (mode.searching)
36715
+ return "type to filter \xB7 \u23CE apply search \xB7 Esc cancel search";
36716
+ return [
36717
+ authorError ? `Could not load authors: ${authorError}` : undefined,
36718
+ pageError ? `Could not apply filter: ${pageError}` : undefined,
36719
+ "\u2191\u2193/jk move \xB7 space toggle \xB7 \u2190\u2192/hl mark \xB7 s search \xB7 \u23CE apply \xB7 Esc cancel"
36720
+ ].filter((value) => value !== undefined).join(" \xB7 ");
36721
+ }
36104
36722
  function truncate(value, width) {
36105
36723
  if (value.length <= width)
36106
36724
  return value;
@@ -36111,87 +36729,33 @@ async function createDefaultRenderer() {
36111
36729
  }
36112
36730
 
36113
36731
  // src/tui/new-session-wizard.ts
36114
- 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";
36115
-
36116
- // src/tui/composer.ts
36117
- import { BoxRenderable as BoxRenderable2, TextRenderable as TextRenderable2, TextareaRenderable } from "@opentui/core";
36732
+ import { bg as bg3, BoxRenderable as BoxRenderable3, bold as bold3, CliRenderEvents as CliRenderEvents3, dim as dim4, fg as fg6, ScrollBoxRenderable as ScrollBoxRenderable2, StyledText as StyledText5, stringToStyledText as stringToStyledText4, TextRenderable as TextRenderable3 } from "@opentui/core";
36118
36733
 
36119
- // src/tui/composer-height.ts
36120
- var composerMaxRows = 6;
36121
- function resizeComposer(composer) {
36122
- composer.height = Math.min(Math.max(composer.editorView.getTotalVirtualLineCount(), 1), composerMaxRows);
36734
+ // src/tui/attachment-presentation.ts
36735
+ import { dim as dim3, fg as fg4, StyledText as StyledText3 } from "@opentui/core";
36736
+ function attachmentUploadMessage(filenames) {
36737
+ return filenames.length === 1 ? `Attaching ${filenames[0]}\u2026` : `Attaching ${filenames.length} files\u2026`;
36738
+ }
36739
+ function renderAttachmentFeedback(feedback) {
36740
+ const presentation = feedback.kind === "progress" ? { glyph: "\u28FF", color: PALETTE.progress } : feedback.kind === "success" ? { glyph: "\u2713", color: PALETTE.statusCompleted } : { glyph: "\u2717", color: PALETTE.failure };
36741
+ return new StyledText3([
36742
+ fg4(presentation.color)(`${presentation.glyph} `),
36743
+ fg4(feedback.kind === "failure" ? PALETTE.failure : PALETTE.bodyText)(feedback.message)
36744
+ ]);
36123
36745
  }
36124
-
36125
- // src/tui/composer.ts
36126
- var defaultPlaceholder = "Message Remy\u2026 Tab paths attach \xB7 Enter send \xB7 Shift+Enter/Ctrl+J newline";
36127
- function createComposer({
36128
- renderer,
36129
- parent,
36130
- tag,
36131
- topDividerLabel,
36132
- placeholder = defaultPlaceholder,
36133
- onSubmit,
36134
- onContentChange
36135
- }) {
36136
- const root = new BoxRenderable2(renderer, { width: "100%", flexDirection: "column", flexShrink: 0 });
36137
- const dividerTop = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
36138
- const textarea = new TextareaRenderable(renderer, {
36139
- width: "100%",
36140
- height: 1,
36141
- flexShrink: 0,
36142
- wrapMode: "word",
36143
- placeholder,
36144
- keyBindings: [
36145
- { name: "return", action: "submit" },
36146
- { name: "return", shift: true, action: "newline" },
36147
- { name: "j", ctrl: true, action: "newline" }
36148
- ],
36149
- onSubmit
36150
- });
36151
- const dividerBottom = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
36152
- let mounted = false;
36153
- root.add(dividerTop);
36154
- root.add(textarea);
36155
- root.add(dividerBottom);
36156
- textarea.onContentChange = () => {
36157
- resizeComposer(textarea);
36158
- onContentChange?.();
36159
- };
36160
- function render() {
36161
- dividerTop.content = renderComposerDivider({ width: renderer.width - 2, tag, leadLabel: topDividerLabel?.() });
36162
- dividerBottom.content = renderComposerDivider({ width: renderer.width - 2 });
36163
- }
36164
- return {
36165
- textarea,
36166
- mount() {
36167
- if (mounted)
36168
- return;
36169
- mounted = true;
36170
- parent.add(root);
36171
- resizeComposer(textarea);
36172
- render();
36173
- },
36174
- unmount() {
36175
- if (!mounted)
36176
- return;
36177
- mounted = false;
36178
- parent.remove(root);
36179
- },
36180
- render,
36181
- focus() {
36182
- textarea.focus();
36183
- },
36184
- destroy() {
36185
- if (mounted)
36186
- parent.remove(root);
36187
- mounted = false;
36188
- root.destroyRecursively();
36189
- }
36190
- };
36746
+ function renderAttachmentSummary(attachments) {
36747
+ if (attachments.length === 0)
36748
+ return new StyledText3([]);
36749
+ const label = attachments.length === 1 ? "Attachment" : `${attachments.length} attachments`;
36750
+ return new StyledText3([
36751
+ dim3(fg4(PALETTE.dimText)("\u21B3 ")),
36752
+ fg4(PALETTE.tool)(label),
36753
+ dim3(fg4(PALETTE.dimText)(` \xB7 ${attachments.map((attachment) => attachment.filename).join(" \xB7 ")}`))
36754
+ ]);
36191
36755
  }
36192
36756
 
36193
36757
  // src/tui/path-completion-menu.ts
36194
- import { bg as bg2, fg as fg4, stringToStyledText as stringToStyledText3, StyledText as StyledText3 } from "@opentui/core";
36758
+ import { bg as bg2, fg as fg5, stringToStyledText as stringToStyledText3, StyledText as StyledText4 } from "@opentui/core";
36195
36759
  var maximumVisibleCompletions = 8;
36196
36760
  function renderPathCompletionMenu({ completions, selectedIndex }) {
36197
36761
  if (completions.length === 0)
@@ -36202,7 +36766,7 @@ function renderPathCompletionMenu({ completions, selectedIndex }) {
36202
36766
  const completionIndex = firstIndex + index;
36203
36767
  const selected = completionIndex === selectedIndex;
36204
36768
  const rowStyle = selected ? bg2(PALETTE.selectionBg) : (chunk) => chunk;
36205
- return new StyledText3([rowStyle(fg4(PALETTE.bodyText)(`${selected ? "\u203A " : " "}${completion.display}`))]);
36769
+ return new StyledText4([rowStyle(fg5(PALETTE.bodyText)(`${selected ? "\u203A " : " "}${completion.display}`))]);
36206
36770
  });
36207
36771
  const overflow = completions.length > visibleCompletions.length ? stringToStyledText3(`${firstIndex + 1}-${firstIndex + visibleCompletions.length} of ${completions.length}`) : undefined;
36208
36772
  return joinStyled([
@@ -36242,13 +36806,62 @@ async function completePathToken({ text, cwd, homeDirectory = homedir() }) {
36242
36806
  const normalized = replacementPath.split(sep).join("/");
36243
36807
  const suffix = entry.isDirectory() ? "/" : "";
36244
36808
  const completed = `${normalized}${suffix}`;
36245
- return { display: completed, replacement: `${token.startsWith("@") ? "@" : ""}${completed}` };
36809
+ return {
36810
+ display: completed,
36811
+ replacement: `${token.startsWith("@") ? "@" : ""}${completed}`,
36812
+ ...entry.isFile() ? { attachmentPath: completedPath } : {}
36813
+ };
36246
36814
  });
36247
36815
  }
36248
36816
  function replaceActivePathToken({ text, replacement }) {
36249
36817
  const token = activePathToken(text);
36250
36818
  return token === undefined ? text : `${text.slice(0, text.length - token.length)}${replacement}`;
36251
36819
  }
36820
+ function selectedAttachmentPaths({ text, selections }) {
36821
+ return selections.filter((selection) => text.slice(selection.start, selection.end) === selection.token && hasPathTokenBoundaries({ text, selection })).map((selection) => selection.attachmentPath);
36822
+ }
36823
+ function createSelectedPathCompletion({ attachmentPath, token, end }) {
36824
+ return { attachmentPath, token, start: end - token.length, end };
36825
+ }
36826
+ function reconcileSelectedPathCompletions({
36827
+ previousText,
36828
+ text,
36829
+ selections
36830
+ }) {
36831
+ if (text === previousText)
36832
+ return selections;
36833
+ let changeStart = 0;
36834
+ while (changeStart < previousText.length && changeStart < text.length && previousText[changeStart] === text[changeStart])
36835
+ changeStart += 1;
36836
+ let previousChangeEnd = previousText.length;
36837
+ let nextChangeEnd = text.length;
36838
+ while (previousChangeEnd > changeStart && nextChangeEnd > changeStart && previousText[previousChangeEnd - 1] === text[nextChangeEnd - 1]) {
36839
+ previousChangeEnd -= 1;
36840
+ nextChangeEnd -= 1;
36841
+ }
36842
+ const offsetChange = nextChangeEnd - previousChangeEnd;
36843
+ return selections.flatMap((selection) => {
36844
+ if (selection.end <= changeStart)
36845
+ return [selection];
36846
+ if (selection.start >= previousChangeEnd) {
36847
+ return [{
36848
+ ...selection,
36849
+ start: selection.start + offsetChange,
36850
+ end: selection.end + offsetChange
36851
+ }];
36852
+ }
36853
+ return [];
36854
+ }).filter((selection) => hasPathTokenBoundaries({ text, selection }));
36855
+ }
36856
+ function hasPathTokenBoundaries({ text, selection }) {
36857
+ let before = selection.start;
36858
+ while (before > 0 && "([{'\"`".includes(text[before - 1]))
36859
+ before -= 1;
36860
+ let after = selection.end;
36861
+ while (after < text.length && '),.;:!?]}"`'.includes(text[after]))
36862
+ after += 1;
36863
+ return (before === 0 || /\s/.test(text[before - 1])) && (after === text.length || /\s/.test(text[after]));
36864
+ }
36252
36865
  function activePathToken(text) {
36253
36866
  const match = text.match(/(?:^|\s)(\S*)$/);
36254
36867
  return match?.[1];
@@ -36261,6 +36874,7 @@ async function createNewSessionWizard({
36261
36874
  repositories,
36262
36875
  reloadRepositories,
36263
36876
  suggestRepositories,
36877
+ suggestBranches,
36264
36878
  readClipboardImage,
36265
36879
  savePastedImage,
36266
36880
  attachPromptPaths,
@@ -36297,15 +36911,19 @@ async function createNewSessionWizard({
36297
36911
  else
36298
36912
  overrides.set(repositoryId, isSelected);
36299
36913
  }, renderRepositoryLoadingSkeleton = function() {
36300
- return new StyledText4([
36301
- dim3(fg5(PALETTE.dimText)(`\u283F \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
36914
+ return new StyledText5([
36915
+ dim4(fg6(PALETTE.dimText)(`\u283F \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
36302
36916
  \u283F \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
36303
36917
  \u283F \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588`))
36304
36918
  ]);
36305
36919
  }, render = function() {
36306
36920
  const repositoryListVisible = step === "repositories" || step === "repositorySearch";
36307
36921
  const composerVisible = step === "prompt" || step === "repositorySearch" || step === "recompute";
36308
- const orient = (body) => joinStyled([renderStepIndicator({ step, hasInstallationChoice: installationIds.length > 1 }), body], `
36922
+ const orient = (body) => joinStyled([renderStepIndicator({
36923
+ step,
36924
+ hasInstallationChoice: installationIds.length > 1,
36925
+ hasBranchChoice: step === "loadingBranches" || branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch")
36926
+ }), body], `
36309
36927
  `);
36310
36928
  if (composerVisible && !composerMounted) {
36311
36929
  composerMounted = true;
@@ -36323,19 +36941,22 @@ async function createNewSessionWizard({
36323
36941
  repositoryRows.content = "";
36324
36942
  repositoryDivider.content = "";
36325
36943
  repositoryFooter.content = "";
36326
- const attachmentText = attachments.length > 0 ? stringToStyledText4(`Attachments: ${attachments.map((attachment) => attachment.filename).join(", ")}`) : undefined;
36944
+ const attachmentText = renderAttachmentSummary(attachments);
36945
+ const attachmentStatus = attachmentFeedback ? renderAttachmentFeedback(attachmentFeedback) : undefined;
36327
36946
  if (step === "prompt") {
36328
36947
  const parts = [
36329
- new StyledText4([stepHeader("New remote Codex session")]),
36948
+ new StyledText5([stepHeader("New remote Codex session")]),
36330
36949
  stringToStyledText4("Describe the work you want Remy to do. Press Enter to continue; Esc back.")
36331
36950
  ];
36332
- if (attachmentText)
36951
+ if (attachmentText.chunks.length > 0)
36333
36952
  parts.push(attachmentText);
36334
36953
  const completionMenu = pathCompletionMenuOpen ? renderPathCompletionMenu({ completions: pathCompletions, selectedIndex: completionIndex }) : undefined;
36335
36954
  if (completionMenu)
36336
36955
  parts.push(completionMenu);
36337
36956
  if (status)
36338
36957
  parts.push(stringToStyledText4(status));
36958
+ if (attachmentStatus)
36959
+ parts.push(attachmentStatus);
36339
36960
  content.content = orient(joinStyled(parts, `
36340
36961
 
36341
36962
  `));
@@ -36343,7 +36964,7 @@ async function createNewSessionWizard({
36343
36964
  const rows = joinStyled(installationIds.map((id, index) => renderSelectableRow({ label: id, isCursor: index === installationIndex })), `
36344
36965
  `);
36345
36966
  content.content = orient(joinStyled([
36346
- new StyledText4([stepHeader("Select GitHub installation")]),
36967
+ new StyledText5([stepHeader("Select GitHub installation")]),
36347
36968
  rows,
36348
36969
  stringToStyledText4("\u2191\u2193 move \xB7 \u23CE continue \xB7 esc back")
36349
36970
  ], `
@@ -36352,11 +36973,11 @@ async function createNewSessionWizard({
36352
36973
  } else if (step === "loadingRepositories") {
36353
36974
  const failed = repositoryLoadState === "failed";
36354
36975
  const parts = [
36355
- new StyledText4([stepHeader("Select repositories")]),
36976
+ new StyledText5([stepHeader("Select repositories")]),
36356
36977
  stringToStyledText4(failed ? "Could not load your repositories." : "Loading your repos\u2026")
36357
36978
  ];
36358
36979
  if (failed) {
36359
- parts.push(new StyledText4([dim3(fg5(PALETTE.dimText)(`(${status})`))]));
36980
+ parts.push(new StyledText5([dim4(fg6(PALETTE.dimText)(`(${status})`))]));
36360
36981
  parts.push(stringToStyledText4("r retry \xB7 esc back"));
36361
36982
  } else {
36362
36983
  parts.push(renderRepositoryLoadingSkeleton());
@@ -36367,12 +36988,41 @@ async function createNewSessionWizard({
36367
36988
  `));
36368
36989
  } else if (step === "loadingSuggestions") {
36369
36990
  content.content = orient(joinStyled([
36370
- new StyledText4([stepHeader("Select repositories")]),
36371
- new StyledText4([fg5(PALETTE.progress)(`${suggestionSpinnerFrames[suggestionSpinnerFrame]} Remy is matching your prompt to repositories\u2026`)]),
36372
- new StyledText4([dim3(fg5(PALETTE.dimText)("This can take a few seconds."))]),
36991
+ new StyledText5([stepHeader("Select repositories")]),
36992
+ new StyledText5([fg6(PALETTE.progress)(`${suggestionSpinnerFrames[suggestionSpinnerFrame]} Remy is matching your prompt to repositories\u2026`)]),
36993
+ new StyledText5([dim4(fg6(PALETTE.dimText)("This can take a few seconds."))]),
36373
36994
  stringToStyledText4("s select repositories yourself \xB7 esc back")
36374
36995
  ], `
36375
36996
 
36997
+ `));
36998
+ } else if (step === "loadingBranches") {
36999
+ content.content = orient(joinStyled([
37000
+ new StyledText5([stepHeader("Choose branches")]),
37001
+ new StyledText5([fg6(PALETTE.progress)(`${suggestionSpinnerFrames[suggestionSpinnerFrame]} Remy is checking for related open pull requests\u2026`)]),
37002
+ new StyledText5([dim4(fg6(PALETTE.dimText)("This can take a few seconds."))]),
37003
+ stringToStyledText4("esc back")
37004
+ ], `
37005
+
37006
+ `));
37007
+ } else if (step === "branches") {
37008
+ const existing = branchSuggestions.filter((suggestion) => suggestion.suggestionKind !== "createSessionBranch");
37009
+ const parts = [new StyledText5([stepHeader("Choose branches")])];
37010
+ if (existing.length > 0) {
37011
+ parts.push(stringToStyledText4("Remy found related open pull-request work:"));
37012
+ for (const suggestion of existing) {
37013
+ const repository = installationRepositories().find((candidate) => candidate.id === suggestion.repositoryId);
37014
+ parts.push(stringToStyledText4(`${repository?.fullName ?? suggestion.repositoryId} \u2014 ${suggestion.branchName}${suggestion.pullRequest ? ` (PR #${suggestion.pullRequest.number}: ${suggestion.pullRequest.title})` : ""}`));
37015
+ }
37016
+ parts.push(new StyledText5(renderSelectableRow({ label: "Continue from the suggested branch", isCursor: branchChoice === "existing" })));
37017
+ parts.push(new StyledText5(renderSelectableRow({ label: "Create a new session branch", isCursor: branchChoice === "new" })));
37018
+ parts.push(stringToStyledText4("\u2191\u2193 choose \xB7 \u23CE continue \xB7 esc back"));
37019
+ } else {
37020
+ parts.push(stringToStyledText4("Remy recommends creating a new session branch."));
37021
+ parts.push(new StyledText5([dim4(fg6(PALETTE.dimText)(status || "No related open pull request was identified from your request."))]));
37022
+ parts.push(stringToStyledText4("\u23CE continue \xB7 esc back"));
37023
+ }
37024
+ content.content = orient(joinStyled(parts, `
37025
+
36376
37026
  `));
36377
37027
  } else if (repositoryListVisible) {
36378
37028
  const selectionOverrides = searchSelections ?? manualSelections;
@@ -36387,15 +37037,17 @@ async function createNewSessionWizard({
36387
37037
  `) : step === "repositorySearch" ? "No matching repositories." : "No repositories available for this installation.";
36388
37038
  repositoryDivider.content = renderComposerDivider({ width: renderer.width - 2 });
36389
37039
  repositoryFooter.content = step === "repositorySearch" ? "type to filter \xB7 \u23CE apply \xB7 esc cancel search" : "\u2191\u2193 move \xB7 space toggle \xB7 \u2190\u2192 mark \xB7 s search \xB7 r recompute \xB7 \u23CE continue \xB7 esc back";
36390
- const parts = step === "repositorySearch" ? [new StyledText4([stepHeader("Search repositories")]), stringToStyledText4("Fuzzy matching repository names.")] : [
36391
- new StyledText4([stepHeader("Select repositories")]),
37040
+ const parts = step === "repositorySearch" ? [new StyledText5([stepHeader("Search repositories")]), stringToStyledText4("Fuzzy matching repository names.")] : [
37041
+ new StyledText5([stepHeader("Select repositories")]),
36392
37042
  stringToStyledText4(`${selected.size} selected \xB7 ${suggestedIds.size} suggested by Remy${suggestionConfidence ? ` (${suggestionConfidence} confidence)` : ""}`),
36393
37043
  stringToStyledText4("Suggestions are based on your prompt and recompute instruction.")
36394
37044
  ];
36395
- if (attachmentText)
37045
+ if (attachmentText.chunks.length > 0)
36396
37046
  parts.push(attachmentText);
36397
37047
  if (status && step === "repositories")
36398
37048
  parts.push(stringToStyledText4(status));
37049
+ if (attachmentStatus)
37050
+ parts.push(attachmentStatus);
36399
37051
  content.content = orient(joinStyled(parts, `
36400
37052
 
36401
37053
  `));
@@ -36403,11 +37055,11 @@ async function createNewSessionWizard({
36403
37055
  editor.placeholder = "Filter repositories\u2026";
36404
37056
  } else if (step === "recompute") {
36405
37057
  const parts = [
36406
- new StyledText4([stepHeader("Recompute suggestions")]),
37058
+ new StyledText5([stepHeader("Recompute suggestions")]),
36407
37059
  stringToStyledText4("Suggestions use your prompt and instruction. Existing manual toggles stay selected or unselected."),
36408
37060
  stringToStyledText4("Enter instruction, then press Enter to continue. Esc back.")
36409
37061
  ];
36410
- if (attachmentText)
37062
+ if (attachmentText.chunks.length > 0)
36411
37063
  parts.push(attachmentText);
36412
37064
  content.content = orient(joinStyled(parts, `
36413
37065
 
@@ -36415,26 +37067,31 @@ async function createNewSessionWizard({
36415
37067
  editor.placeholder = "e.g. worker, migration, dashboard";
36416
37068
  } else {
36417
37069
  const selected = installationRepositories().filter((repository) => selectedRepositoryIds().has(repository.id));
36418
- const meta5 = new StyledText4([
36419
- fg5(PALETTE.bodyText)(`Installation: ${selectedInstallationId() ?? "(missing)"}
37070
+ const branchOverrides = branchChoice === "existing" ? branchSuggestions.filter((suggestion) => suggestion.suggestionKind !== "createSessionBranch") : [];
37071
+ const meta5 = new StyledText5([
37072
+ fg6(PALETTE.bodyText)(`Installation: ${selectedInstallationId() ?? "(missing)"}
37073
+ `),
37074
+ fg6(PALETTE.bodyText)(`Repositories: ${selected.map((repository) => repository.fullName).join(", ") || "(none)"}
36420
37075
  `),
36421
- fg5(PALETTE.bodyText)(`Repositories: ${selected.map((repository) => repository.fullName).join(", ") || "(none)"}
37076
+ fg6(PALETTE.bodyText)(`Branches: ${branchOverrides.length > 0 ? branchOverrides.map((override) => override.branchName).join(", ") : "new session branches"}
36422
37077
  `),
36423
- fg5(PALETTE.bodyText)(`Model: ${newSessionModelLabel(model)} \xB7 ${newSessionReasoningEffortLabel(reasoningEffort)} reasoning`),
36424
- dim3(fg5(PALETTE.dimText)(" \u25B8 m model \xB7 \u2190\u2192 reasoning"))
37078
+ fg6(PALETTE.bodyText)(`Model: ${newSessionModelLabel(model)} \xB7 ${newSessionReasoningEffortLabel(reasoningEffort)} reasoning`),
37079
+ dim4(fg6(PALETTE.dimText)(" \u25B8 m model \xB7 \u2190\u2192 reasoning"))
36425
37080
  ]);
36426
37081
  const parts = [
36427
- new StyledText4([stepHeader("Confirm new session")]),
36428
- new StyledText4([renderRoleLabel({ label: "You", role: "human" })]),
37082
+ new StyledText5([stepHeader("Confirm new session")]),
37083
+ new StyledText5([renderRoleLabel({ label: "You", role: "human" })]),
36429
37084
  renderMessageBody({ text: prompt, role: "human" }),
36430
37085
  meta5
36431
37086
  ];
36432
- if (attachmentText)
37087
+ if (attachmentText.chunks.length > 0)
36433
37088
  parts.push(attachmentText);
36434
- parts.push(new StyledText4([dim3(fg5(PALETTE.dimText)("Remy will clone the selected repositories, work on a new branch, and open a pull request; you land in the session view."))]));
36435
- parts.push(stringToStyledText4("\u23CE create \xB7 e edit prompt \xB7 r repositories \xB7 esc back"));
37089
+ parts.push(new StyledText5([dim4(fg6(PALETTE.dimText)(branchOverrides.length > 0 ? "Remy will continue on the selected existing branch and update its pull request; you land in the session view." : "Remy will clone the selected repositories, work on new session branches, and open a pull request for each repository; you land in the session view."))]));
37090
+ parts.push(stringToStyledText4(branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch") ? "\u23CE create \xB7 e edit prompt \xB7 r repositories \xB7 b branches \xB7 esc back" : "\u23CE create \xB7 e edit prompt \xB7 r repositories \xB7 esc back"));
36436
37091
  if (status)
36437
37092
  parts.push(stringToStyledText4(status));
37093
+ if (attachmentStatus)
37094
+ parts.push(attachmentStatus);
36438
37095
  content.content = orient(joinStyled(parts, `
36439
37096
 
36440
37097
  `));
@@ -36459,7 +37116,7 @@ async function createNewSessionWizard({
36459
37116
  syncSuggestionLoadingIndicator();
36460
37117
  renderer.requestRender();
36461
37118
  }, syncSuggestionLoadingIndicator = function() {
36462
- if (destroyed || settled || step !== "loadingSuggestions") {
37119
+ if (destroyed || settled || step !== "loadingSuggestions" && step !== "loadingBranches") {
36463
37120
  if (suggestionSpinnerTimer) {
36464
37121
  clearInterval(suggestionSpinnerTimer);
36465
37122
  suggestionSpinnerTimer = undefined;
@@ -36470,22 +37127,28 @@ async function createNewSessionWizard({
36470
37127
  if (suggestionSpinnerTimer)
36471
37128
  return;
36472
37129
  suggestionSpinnerTimer = setInterval(() => {
36473
- if (destroyed || settled || step !== "loadingSuggestions") {
37130
+ if (destroyed || settled || step !== "loadingSuggestions" && step !== "loadingBranches") {
36474
37131
  syncSuggestionLoadingIndicator();
36475
37132
  return;
36476
37133
  }
36477
37134
  suggestionSpinnerFrame = (suggestionSpinnerFrame + 1) % suggestionSpinnerFrames.length;
36478
37135
  render();
36479
37136
  }, suggestionSpinnerIntervalMs);
37137
+ }, invalidatePendingConfirmation = function() {
37138
+ pendingConfirmation = undefined;
37139
+ if (attachmentFeedback?.kind === "progress")
37140
+ attachmentFeedback = undefined;
36480
37141
  }, finish = function(value) {
36481
37142
  if (settled)
36482
37143
  return;
37144
+ invalidatePendingConfirmation();
36483
37145
  settled = true;
36484
37146
  syncSuggestionLoadingIndicator();
36485
37147
  resolveDraft(value);
36486
37148
  }, fail = function(error93) {
36487
37149
  if (settled)
36488
37150
  return;
37151
+ invalidatePendingConfirmation();
36489
37152
  settled = true;
36490
37153
  syncSuggestionLoadingIndicator();
36491
37154
  rejectDraft(error93);
@@ -36556,7 +37219,15 @@ async function createNewSessionWizard({
36556
37219
  returnToPromptFromRepositoryLoading();
36557
37220
  return;
36558
37221
  }
37222
+ if (step === "loadingBranches" || step === "branches") {
37223
+ branchRequestGeneration += 1;
37224
+ step = "repositories";
37225
+ render();
37226
+ return;
37227
+ }
36559
37228
  if (step === "recompute" || step === "confirmation") {
37229
+ if (step === "confirmation")
37230
+ invalidatePendingConfirmation();
36560
37231
  step = "repositories";
36561
37232
  editor.setText("");
36562
37233
  render();
@@ -36584,6 +37255,15 @@ async function createNewSessionWizard({
36584
37255
  const completedText = replaceActivePathToken({ text: editor.plainText, replacement: completion.replacement });
36585
37256
  editor.setText(completedText);
36586
37257
  editor.cursorOffset = completedText.length;
37258
+ if (completion.attachmentPath) {
37259
+ const selectedCompletion = createSelectedPathCompletion({
37260
+ attachmentPath: completion.attachmentPath,
37261
+ token: completion.replacement,
37262
+ end: completedText.length
37263
+ });
37264
+ selectedPathCompletions = [...selectedPathCompletions, selectedCompletion];
37265
+ }
37266
+ previousPromptEditorText = completedText;
36587
37267
  }
36588
37268
  pathCompletions = [];
36589
37269
  pathCompletionMenuOpen = false;
@@ -36609,12 +37289,14 @@ async function createNewSessionWizard({
36609
37289
  if (step === "confirmation") {
36610
37290
  if (key.name === "m") {
36611
37291
  key.preventDefault();
37292
+ invalidatePendingConfirmation();
36612
37293
  model = cycle({ values: newSessionModels, value: model, direction: 1 });
36613
37294
  render();
36614
37295
  return;
36615
37296
  }
36616
37297
  if (key.name === "left" || key.name === "right") {
36617
37298
  key.preventDefault();
37299
+ invalidatePendingConfirmation();
36618
37300
  reasoningEffort = clampedStep({
36619
37301
  values: newSessionReasoningEfforts,
36620
37302
  value: reasoningEffort,
@@ -36625,6 +37307,7 @@ async function createNewSessionWizard({
36625
37307
  }
36626
37308
  if (key.name === "e") {
36627
37309
  key.preventDefault();
37310
+ invalidatePendingConfirmation();
36628
37311
  step = "prompt";
36629
37312
  editor.setText(prompt);
36630
37313
  render();
@@ -36632,11 +37315,19 @@ async function createNewSessionWizard({
36632
37315
  }
36633
37316
  if (key.name === "r") {
36634
37317
  key.preventDefault();
37318
+ invalidatePendingConfirmation();
36635
37319
  step = "repositories";
36636
37320
  editor.setText("");
36637
37321
  render();
36638
37322
  return;
36639
37323
  }
37324
+ if (key.name === "b" && branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch")) {
37325
+ key.preventDefault();
37326
+ invalidatePendingConfirmation();
37327
+ step = "branches";
37328
+ render();
37329
+ return;
37330
+ }
36640
37331
  }
36641
37332
  if (step === "repositorySearch")
36642
37333
  return;
@@ -36657,8 +37348,16 @@ async function createNewSessionWizard({
36657
37348
  return;
36658
37349
  }
36659
37350
  }
37351
+ if (step === "branches" && branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch") && (key.name === "up" || key.name === "down" || key.name === "left" || key.name === "right")) {
37352
+ key.preventDefault();
37353
+ branchChoice = branchChoice === "existing" ? "new" : "existing";
37354
+ render();
37355
+ return;
37356
+ }
36660
37357
  if (!composerMounted && (key.name === "return" || key.name === "enter")) {
36661
37358
  key.preventDefault();
37359
+ if (step === "loadingBranches")
37360
+ return;
36662
37361
  submitEditor();
36663
37362
  return;
36664
37363
  }
@@ -36747,6 +37446,7 @@ async function createNewSessionWizard({
36747
37446
  let repositoryLoadState = Array.isArray(repositories) ? "ready" : "loading";
36748
37447
  let repositoryRequestGeneration = 0;
36749
37448
  let suggestionRequestGeneration = 0;
37449
+ let branchRequestGeneration = 0;
36750
37450
  let installationIds = [...new Set(loadedRepositories.map((repository) => repository.githubInstallationId))].sort((left, right) => left.localeCompare(right));
36751
37451
  let step = initialDraft ? "confirmation" : "prompt";
36752
37452
  let prompt = initialDraft?.prompt ?? "";
@@ -36758,7 +37458,9 @@ async function createNewSessionWizard({
36758
37458
  let recomputeInstruction = "";
36759
37459
  let suggestionSpinnerFrame = 0;
36760
37460
  let attachments = [...initialDraft?.attachments ?? []];
37461
+ let previousPromptEditorText = "";
36761
37462
  let pathCompletions = [];
37463
+ let selectedPathCompletions = [];
36762
37464
  let completionIndex = 0;
36763
37465
  let pathCompletionMenuOpen = false;
36764
37466
  let repositoryHeaderHeight;
@@ -36767,8 +37469,17 @@ async function createNewSessionWizard({
36767
37469
  let searchSelections;
36768
37470
  let suggestedIds = new Set;
36769
37471
  let suggestionConfidence;
37472
+ let branchSuggestions = initialDraft?.repositoryBranchOverrides?.map((override) => ({
37473
+ suggestionKind: "continueExistingBranch",
37474
+ repositoryId: override.repositoryId,
37475
+ branchName: override.branchName,
37476
+ pullRequest: null
37477
+ })) ?? [];
37478
+ let branchChoice = initialDraft?.repositoryBranchOverrides?.length ? "existing" : "new";
36770
37479
  let status = initialError ?? "";
37480
+ let attachmentFeedback;
36771
37481
  let pendingPastedImageWrites = 0;
37482
+ let pendingConfirmation;
36772
37483
  let destroyed = false;
36773
37484
  let rendererDestroyPromise;
36774
37485
  let settled = false;
@@ -36791,6 +37502,14 @@ async function createNewSessionWizard({
36791
37502
  submitEditor();
36792
37503
  },
36793
37504
  onContentChange: () => {
37505
+ if (step === "prompt") {
37506
+ selectedPathCompletions = reconcileSelectedPathCompletions({
37507
+ previousText: previousPromptEditorText,
37508
+ text: editor.plainText,
37509
+ selections: selectedPathCompletions
37510
+ });
37511
+ previousPromptEditorText = editor.plainText;
37512
+ }
36794
37513
  pathCompletionMenuOpen = false;
36795
37514
  if (step === "repositorySearch") {
36796
37515
  repositoryQuery = editor.plainText;
@@ -36826,6 +37545,35 @@ async function createNewSessionWizard({
36826
37545
  status = error93 instanceof Error ? error93.message : String(error93);
36827
37546
  }
36828
37547
  }
37548
+ async function startBranchReview() {
37549
+ const installationId = selectedInstallationId();
37550
+ if (!installationId)
37551
+ return;
37552
+ const repositoryIds = [...selectedRepositoryIds()];
37553
+ const requestGeneration = ++branchRequestGeneration;
37554
+ branchSuggestions = [];
37555
+ branchChoice = "new";
37556
+ step = "loadingBranches";
37557
+ status = "";
37558
+ render();
37559
+ try {
37560
+ const suggestions = await suggestBranches({ installationId, prompt, repositoryIds });
37561
+ if (destroyed || requestGeneration !== branchRequestGeneration || step !== "loadingBranches")
37562
+ return;
37563
+ branchSuggestions = suggestions;
37564
+ const hasExistingBranch = branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch");
37565
+ branchChoice = hasExistingBranch ? "existing" : "new";
37566
+ step = hasExistingBranch ? "branches" : "confirmation";
37567
+ } catch (error93) {
37568
+ if (destroyed || requestGeneration !== branchRequestGeneration || step !== "loadingBranches")
37569
+ return;
37570
+ branchSuggestions = [];
37571
+ branchChoice = "new";
37572
+ status = error93 instanceof Error ? error93.message : String(error93);
37573
+ step = "confirmation";
37574
+ }
37575
+ render();
37576
+ }
36829
37577
  async function openPathCompletionMenu() {
36830
37578
  if (destroyed)
36831
37579
  return;
@@ -36857,28 +37605,33 @@ async function createNewSessionWizard({
36857
37605
  }
36858
37606
  async function pasteClipboardImage() {
36859
37607
  if (!readClipboardImage || !savePastedImage) {
36860
- status = "Clipboard image paste is unavailable.";
37608
+ attachmentFeedback = { kind: "failure", message: "Clipboard image paste is unavailable." };
36861
37609
  render();
36862
37610
  return;
36863
37611
  }
36864
37612
  pendingPastedImageWrites += 1;
36865
- status = "Saving clipboard image\u2026";
37613
+ attachmentFeedback = { kind: "progress", message: "Saving clipboard image\u2026" };
36866
37614
  render();
36867
37615
  try {
36868
37616
  const image = await readClipboardImage();
36869
37617
  const path = await savePastedImage(image);
36870
37618
  if (!destroyed) {
36871
37619
  editor.insertText(path);
36872
- status = "Clipboard image added; continue to attach it.";
37620
+ const selectedCompletion = createSelectedPathCompletion({ attachmentPath: path, token: path, end: editor.cursorOffset });
37621
+ selectedPathCompletions = [...selectedPathCompletions, selectedCompletion];
37622
+ previousPromptEditorText = editor.plainText;
37623
+ attachmentFeedback = { kind: "success", message: "Clipboard image ready to attach." };
36873
37624
  }
36874
37625
  } catch (error93) {
36875
- status = error93 instanceof Error ? error93.message : String(error93);
37626
+ attachmentFeedback = { kind: "failure", message: error93 instanceof Error ? error93.message : String(error93) };
36876
37627
  } finally {
36877
37628
  pendingPastedImageWrites -= 1;
36878
37629
  }
36879
37630
  render();
36880
37631
  }
36881
37632
  async function confirm() {
37633
+ if (destroyed || settled || step !== "confirmation" || pendingConfirmation)
37634
+ return;
36882
37635
  const installationId = selectedInstallationId();
36883
37636
  if (!installationId) {
36884
37637
  status = "GitHub installation selection is required.";
@@ -36890,22 +37643,55 @@ async function createNewSessionWizard({
36890
37643
  fail(new Error("Selected repositories must belong to the chosen GitHub installation."));
36891
37644
  return;
36892
37645
  }
37646
+ const repositoryBranchOverrides = branchChoice === "existing" ? branchSuggestions.flatMap((suggestion) => suggestion.suggestionKind === "createSessionBranch" ? [] : [{ repositoryId: suggestion.repositoryId, branchName: suggestion.branchName }]) : [];
37647
+ const confirmation = Symbol("confirmation");
37648
+ const confirmedAttachments = [...attachments];
37649
+ const confirmedPrompt = prompt;
37650
+ const confirmedModel = model;
37651
+ const confirmedReasoningEffort = reasoningEffort;
37652
+ const confirmedSelectedPaths = selectedAttachmentPaths({ text: confirmedPrompt, selections: selectedPathCompletions });
37653
+ pendingConfirmation = confirmation;
37654
+ const ownsConfirmation = () => !destroyed && !settled && step === "confirmation" && pendingConfirmation === confirmation;
36893
37655
  if (attachPromptPaths) {
36894
- status = "Attaching referenced files\u2026";
36895
- render();
37656
+ attachmentFeedback = undefined;
36896
37657
  try {
36897
37658
  const promptAttachments = await attachPromptPaths({
36898
- text: prompt,
36899
- excludedPaths: attachments.flatMap((attachment) => attachment.sourcePath ? [attachment.sourcePath] : [])
37659
+ text: confirmedPrompt,
37660
+ excludedPaths: confirmedAttachments.flatMap((attachment) => attachment.sourcePath ? [attachment.sourcePath] : []),
37661
+ selectedPaths: confirmedSelectedPaths,
37662
+ onUploadStart: ({ filenames }) => {
37663
+ if (!ownsConfirmation())
37664
+ return;
37665
+ attachmentFeedback = { kind: "progress", message: attachmentUploadMessage(filenames) };
37666
+ render();
37667
+ }
36900
37668
  });
36901
- attachments = [...attachments, ...promptAttachments];
37669
+ if (!ownsConfirmation())
37670
+ return;
37671
+ confirmedAttachments.push(...promptAttachments);
37672
+ attachments = confirmedAttachments;
37673
+ selectedPathCompletions = [];
37674
+ attachmentFeedback = undefined;
36902
37675
  } catch (error93) {
36903
- status = error93 instanceof Error ? error93.message : String(error93);
37676
+ if (!ownsConfirmation())
37677
+ return;
37678
+ pendingConfirmation = undefined;
37679
+ attachmentFeedback = { kind: "failure", message: error93 instanceof Error ? error93.message : String(error93) };
36904
37680
  render();
36905
37681
  return;
36906
37682
  }
36907
37683
  }
36908
- finish({ prompt, githubInstallationId: installationId, repositories: selected, attachments, model, reasoningEffort });
37684
+ if (!ownsConfirmation())
37685
+ return;
37686
+ finish({
37687
+ prompt: confirmedPrompt,
37688
+ githubInstallationId: installationId,
37689
+ repositories: selected,
37690
+ ...repositoryBranchOverrides.length > 0 ? { repositoryBranchOverrides } : {},
37691
+ attachments: confirmedAttachments,
37692
+ model: confirmedModel,
37693
+ reasoningEffort: confirmedReasoningEffort
37694
+ });
36909
37695
  }
36910
37696
  async function submitEditor() {
36911
37697
  const value = editor.plainText.trim();
@@ -36939,8 +37725,12 @@ async function createNewSessionWizard({
36939
37725
  return;
36940
37726
  }
36941
37727
  if (step === "repositories") {
36942
- step = "confirmation";
36943
37728
  editor.setText("");
37729
+ startBranchReview();
37730
+ return;
37731
+ }
37732
+ if (step === "branches") {
37733
+ step = "confirmation";
36944
37734
  render();
36945
37735
  return;
36946
37736
  }
@@ -37004,6 +37794,7 @@ async function createNewSessionWizard({
37004
37794
  destroy() {
37005
37795
  if (rendererDestroyed2)
37006
37796
  return;
37797
+ invalidatePendingConfirmation();
37007
37798
  destroyed = true;
37008
37799
  if (inputHandler)
37009
37800
  renderer.removeInputHandler(inputHandler);
@@ -37040,12 +37831,13 @@ async function createNewSessionWizard({
37040
37831
  }
37041
37832
  }
37042
37833
  function stepHeader(title) {
37043
- return bold3(fg5(PALETTE.remyAccent)(title));
37834
+ return bold3(fg6(PALETTE.remyAccent)(title));
37044
37835
  }
37045
37836
  var WIZARD_STEPS = [
37046
37837
  { key: "describe", label: "Describe" },
37047
37838
  { key: "installation", label: "Installation" },
37048
37839
  { key: "repositories", label: "Repositories" },
37840
+ { key: "branches", label: "Branches" },
37049
37841
  { key: "confirm", label: "Confirm" }
37050
37842
  ];
37051
37843
  function wizardStepKey(step) {
@@ -37053,52 +37845,41 @@ function wizardStepKey(step) {
37053
37845
  return "describe";
37054
37846
  if (step === "installation")
37055
37847
  return "installation";
37848
+ if (step === "loadingBranches" || step === "branches")
37849
+ return "branches";
37056
37850
  if (step === "confirmation")
37057
37851
  return "confirm";
37058
37852
  return "repositories";
37059
37853
  }
37060
- function renderStepIndicator({ step, hasInstallationChoice }) {
37061
- const steps = WIZARD_STEPS.filter((entry) => entry.key !== "installation" || hasInstallationChoice);
37854
+ function renderStepIndicator({ step, hasInstallationChoice, hasBranchChoice = false }) {
37855
+ const steps = WIZARD_STEPS.filter((entry) => (entry.key !== "installation" || hasInstallationChoice) && (entry.key !== "branches" || hasBranchChoice));
37062
37856
  const currentKey = wizardStepKey(step);
37063
37857
  const currentIndex = steps.findIndex((entry) => entry.key === currentKey);
37064
37858
  const position = currentIndex >= 0 ? currentIndex + 1 : steps.length;
37065
37859
  const crumbs = [];
37066
37860
  steps.forEach((entry, index) => {
37067
37861
  if (index > 0)
37068
- crumbs.push(fg5(PALETTE.dimText)(" \u203A "));
37069
- crumbs.push(entry.key === currentKey ? bold3(fg5(PALETTE.bodyText)(entry.label)) : fg5(PALETTE.dimText)(entry.label));
37862
+ crumbs.push(fg6(PALETTE.dimText)(" \u203A "));
37863
+ crumbs.push(entry.key === currentKey ? bold3(fg6(PALETTE.bodyText)(entry.label)) : fg6(PALETTE.dimText)(entry.label));
37070
37864
  });
37071
- return new StyledText4([
37072
- bold3(fg5(PALETTE.remyAccent)(`Step ${position} of ${steps.length}`)),
37073
- fg5(PALETTE.dimText)(" "),
37865
+ return new StyledText5([
37866
+ bold3(fg6(PALETTE.remyAccent)(`Step ${position} of ${steps.length}`)),
37867
+ fg6(PALETTE.dimText)(" "),
37074
37868
  ...crumbs
37075
37869
  ]);
37076
37870
  }
37077
37871
  function renderSelectableRow({ label, isCursor }) {
37078
37872
  const line = `${isCursor ? "> " : " "}${label}`;
37079
- const styled = fg5(PALETTE.bodyText)(line);
37873
+ const styled = fg6(PALETTE.bodyText)(line);
37080
37874
  return [isCursor ? bg3(PALETTE.selectionBg)(styled) : styled];
37081
37875
  }
37082
37876
  function renderRepositoryRow({ repository, isCursor, isChecked, source }) {
37083
37877
  const marker = isChecked ? "[x]" : "[ ]";
37084
37878
  const line = `${isCursor ? "> " : " "}${marker} ${repository.fullName}`;
37085
- const styled = isChecked ? bold3(fg5(PALETTE.humanAccent)(line)) : fg5(PALETTE.dimText)(line);
37086
- const tag = source === "suggested" && isChecked ? dim3(fg5(PALETTE.tool)(" \u25C6 suggested by Remy")) : undefined;
37879
+ const styled = isChecked ? bold3(fg6(PALETTE.humanAccent)(line)) : fg6(PALETTE.dimText)(line);
37880
+ const tag = source === "suggested" && isChecked ? dim4(fg6(PALETTE.tool)(" \u25C6 suggested by Remy")) : undefined;
37087
37881
  return [isCursor ? bg3(PALETTE.selectionBg)(styled) : styled, ...tag ? [tag] : []];
37088
37882
  }
37089
- function fuzzyMatches(value, query) {
37090
- const normalizedQuery = query.replaceAll(/\s/g, "").toLowerCase();
37091
- if (!normalizedQuery)
37092
- return true;
37093
- let queryIndex = 0;
37094
- for (const character of value.toLowerCase()) {
37095
- if (character === normalizedQuery[queryIndex])
37096
- queryIndex += 1;
37097
- if (queryIndex === normalizedQuery.length)
37098
- return true;
37099
- }
37100
- return false;
37101
- }
37102
37883
  function cycle({ values, value, direction }) {
37103
37884
  const currentIndex = values.indexOf(value);
37104
37885
  const nextIndex = (currentIndex + direction + values.length) % values.length;
@@ -37114,7 +37895,7 @@ async function createDefaultRenderer2() {
37114
37895
  }
37115
37896
 
37116
37897
  // src/tui/session-view.ts
37117
- 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";
37898
+ import { CliRenderEvents as CliRenderEvents4, BoxRenderable as BoxRenderable4, bg as bg4, bold as bold4, dim as dim5, fg as fg7, ScrollBoxRenderable as ScrollBoxRenderable3, StyledText as StyledText6, stringToStyledText as stringToStyledText5, TextRenderable as TextRenderable4 } from "@opentui/core";
37118
37899
  var composerSlashCommands = [
37119
37900
  { value: "/sessions", description: "Back to the session list" },
37120
37901
  { value: "/complete", description: "Finish this session" },
@@ -37195,6 +37976,7 @@ async function createSessionTui({
37195
37976
  }
37196
37977
  promptHistoryIndex = direction === "up" ? Math.max(0, promptHistoryIndex - 1) : Math.min(promptHistory.length, promptHistoryIndex + 1);
37197
37978
  const text = promptHistoryIndex === promptHistory.length ? promptHistoryDraft : promptHistory[promptHistoryIndex];
37979
+ selectedPathCompletions = [];
37198
37980
  historyReplacement = text;
37199
37981
  composer.setText(text);
37200
37982
  composer.cursorOffset = text.length;
@@ -37217,10 +37999,10 @@ async function createSessionTui({
37217
37999
  syncTerminalSessionControls();
37218
38000
  syncWorkingIndicator();
37219
38001
  renderRetainedTranscript();
37220
- const assistantPreview = latestState.previews.assistantText ? new StyledText5([dim4(fg6(PALETTE.dimText)(`Live assistant preview
37221
- `)), fg6(PALETTE.remyAccent)(latestState.previews.assistantText)]) : new StyledText5([]);
37222
- const reasoningPreview = latestState.previews.reasoningText ? new StyledText5([dim4(fg6(PALETTE.dimText)(`Live reasoning preview
37223
- `)), dim4(fg6(PALETTE.dimText)(latestState.previews.reasoningText))]) : new StyledText5([]);
38002
+ const assistantPreview = latestState.previews.assistantText ? new StyledText6([dim5(fg7(PALETTE.dimText)(`Live assistant preview
38003
+ `)), fg7(PALETTE.remyAccent)(latestState.previews.assistantText)]) : new StyledText6([]);
38004
+ const reasoningPreview = latestState.previews.reasoningText ? new StyledText6([dim5(fg7(PALETTE.dimText)(`Live reasoning preview
38005
+ `)), dim5(fg7(PALETTE.dimText)(latestState.previews.reasoningText))]) : new StyledText6([]);
37224
38006
  const working = isRemyWorking(latestState);
37225
38007
  const startedAt = workingTurnStartedAt(latestState);
37226
38008
  const elapsedMs = startedAt === undefined ? 0 : Math.max(0, now3() - Date.parse(startedAt));
@@ -37229,7 +38011,7 @@ async function createSessionTui({
37229
38011
  frame: workingSpinnerFrames[spinnerFrameIndex],
37230
38012
  elapsedMs,
37231
38013
  mode: stopState.kind === "stopping" ? "stopping" : "working"
37232
- }) : new StyledText5([]);
38014
+ }) : new StyledText6([]);
37233
38015
  const liveContent = [assistantPreview, reasoningPreview, liveIndicator].filter((part) => part.chunks.length > 0);
37234
38016
  liveTranscript.content = liveContent.length === 0 ? "" : joinStyled([stringToStyledText5(`
37235
38017
 
@@ -37245,6 +38027,7 @@ async function createSessionTui({
37245
38027
  const commandMenu = renderSlashCommandMenu({ commands: slashCompletions, selectedIndex: slashCompletionIndex });
37246
38028
  const composerStatus = [
37247
38029
  helpVisible ? renderHelpPanel() : undefined,
38030
+ attachmentFeedback ? renderAttachmentFeedback(attachmentFeedback) : undefined,
37248
38031
  composerFeedback ? stringToStyledText5(composerFeedback) : undefined,
37249
38032
  submissionStatus,
37250
38033
  commandMenu,
@@ -37317,10 +38100,17 @@ async function createSessionTui({
37317
38100
  }
37318
38101
  slashCompletions = composerSlashCommands.filter((command) => command.value.startsWith(value) && command.value !== value && (!["/complete", "/cancel"].includes(command.value) || latestState.aggregateStatus === "open"));
37319
38102
  slashCompletionIndex = 0;
38103
+ }, invalidateAttachmentPreparation = function() {
38104
+ attachmentPreparationOwner = undefined;
38105
+ }, ownsAttachmentPreparation = function(owner) {
38106
+ return attachmentPreparationOwner === owner && !destroyed && !settled;
38107
+ }, viewIsActive = function() {
38108
+ return !destroyed && !settled;
37320
38109
  }, finish = function(result, error93) {
37321
38110
  if (settled)
37322
38111
  return;
37323
38112
  settled = true;
38113
+ invalidateAttachmentPreparation();
37324
38114
  if (error93)
37325
38115
  rejectAction(error93);
37326
38116
  else
@@ -37344,17 +38134,21 @@ async function createSessionTui({
37344
38134
  let settled = false;
37345
38135
  let activityExpanded = false;
37346
38136
  let composerDraft = initialComposer ?? { text: "", attachments: [] };
38137
+ let previousComposerText = composerDraft.text;
37347
38138
  let pathCompletions = [];
38139
+ let selectedPathCompletions = [];
37348
38140
  let completionIndex = 0;
37349
38141
  let pathCompletionMenuOpen = false;
37350
38142
  let slashCompletions = [];
37351
38143
  let slashCompletionIndex = 0;
37352
38144
  let composerFeedback = initialFeedback;
38145
+ let attachmentFeedback;
37353
38146
  const localPromptHistory = [];
37354
38147
  let promptHistoryIndex;
37355
38148
  let promptHistoryDraft = "";
37356
38149
  let historyReplacement;
37357
38150
  let pendingPastedImageWrites = 0;
38151
+ let attachmentPreparationOwner;
37358
38152
  let admittedSubmissions = [];
37359
38153
  let stopState = { kind: "idle" };
37360
38154
  let lifecycleRequestState = { kind: "idle" };
@@ -37393,6 +38187,12 @@ async function createSessionTui({
37393
38187
  submitComposer();
37394
38188
  },
37395
38189
  onContentChange: () => {
38190
+ selectedPathCompletions = reconcileSelectedPathCompletions({
38191
+ previousText: previousComposerText,
38192
+ text: composer.plainText,
38193
+ selections: selectedPathCompletions
38194
+ });
38195
+ previousComposerText = composer.plainText;
37396
38196
  if (historyReplacement !== composer.plainText)
37397
38197
  promptHistoryIndex = undefined;
37398
38198
  historyReplacement = undefined;
@@ -37473,12 +38273,12 @@ async function createSessionTui({
37473
38273
  };
37474
38274
  async function pasteClipboardImage() {
37475
38275
  if (!readClipboardImage || !savePastedImage) {
37476
- composerFeedback = "Clipboard image paste is unavailable.";
38276
+ attachmentFeedback = { kind: "failure", message: "Clipboard image paste is unavailable." };
37477
38277
  render();
37478
38278
  return;
37479
38279
  }
37480
38280
  pendingPastedImageWrites += 1;
37481
- composerFeedback = "Saving clipboard image\u2026";
38281
+ attachmentFeedback = { kind: "progress", message: "Saving clipboard image\u2026" };
37482
38282
  render();
37483
38283
  try {
37484
38284
  const image = await readClipboardImage();
@@ -37486,10 +38286,13 @@ async function createSessionTui({
37486
38286
  if (destroyed)
37487
38287
  return;
37488
38288
  composer.insertText(path);
37489
- composerFeedback = "Clipboard image added; send message to attach it.";
38289
+ const selectedCompletion = createSelectedPathCompletion({ attachmentPath: path, token: path, end: composer.cursorOffset });
38290
+ selectedPathCompletions = [...selectedPathCompletions, selectedCompletion];
38291
+ previousComposerText = composer.plainText;
38292
+ attachmentFeedback = { kind: "success", message: "Clipboard image ready to attach." };
37490
38293
  } catch (error93) {
37491
38294
  if (!destroyed)
37492
- composerFeedback = error93 instanceof Error ? error93.message : String(error93);
38295
+ attachmentFeedback = { kind: "failure", message: error93 instanceof Error ? error93.message : String(error93) };
37493
38296
  } finally {
37494
38297
  pendingPastedImageWrites -= 1;
37495
38298
  if (!destroyed)
@@ -37549,6 +38352,15 @@ async function createSessionTui({
37549
38352
  const completedText = replaceActivePathToken({ text: composer.plainText, replacement: completion.replacement });
37550
38353
  composer.setText(completedText);
37551
38354
  composer.cursorOffset = completedText.length;
38355
+ if (completion.attachmentPath) {
38356
+ const selectedCompletion = createSelectedPathCompletion({
38357
+ attachmentPath: completion.attachmentPath,
38358
+ token: completion.replacement,
38359
+ end: completedText.length
38360
+ });
38361
+ selectedPathCompletions = [...selectedPathCompletions, selectedCompletion];
38362
+ }
38363
+ previousComposerText = completedText;
37552
38364
  }
37553
38365
  pathCompletions = [];
37554
38366
  pathCompletionMenuOpen = false;
@@ -37579,8 +38391,10 @@ async function createSessionTui({
37579
38391
  render();
37580
38392
  }
37581
38393
  async function submitComposer() {
38394
+ if (attachmentPreparationOwner)
38395
+ return;
37582
38396
  if (pendingPastedImageWrites > 0) {
37583
- composerFeedback = "Saving pasted image\u2026";
38397
+ attachmentFeedback = { kind: "progress", message: "Saving pasted image\u2026" };
37584
38398
  render();
37585
38399
  return;
37586
38400
  }
@@ -37588,33 +38402,61 @@ async function createSessionTui({
37588
38402
  const value = composerDraft.text;
37589
38403
  if (value.trim() === "")
37590
38404
  return;
37591
- if (await handleComposerCommand(value))
38405
+ const preparationOwner = Symbol("attachment preparation");
38406
+ attachmentPreparationOwner = preparationOwner;
38407
+ if (await handleComposerCommand(value)) {
38408
+ if (attachmentPreparationOwner === preparationOwner)
38409
+ invalidateAttachmentPreparation();
38410
+ return;
38411
+ }
38412
+ if (!ownsAttachmentPreparation(preparationOwner))
37592
38413
  return;
37593
38414
  if (lifecycleRequestState.kind === "pending") {
37594
38415
  composerFeedback = `${lifecycleOperationPresentParticiple(lifecycleRequestState.operation)} the session \u2014 messages are paused.`;
37595
38416
  render();
38417
+ invalidateAttachmentPreparation();
37596
38418
  return;
37597
38419
  }
37598
38420
  if (latestState.aggregateStatus !== "open") {
37599
38421
  composerFeedback = "This session is already terminal. Run /sessions to return to the session list.";
37600
38422
  render();
38423
+ invalidateAttachmentPreparation();
37601
38424
  return;
37602
38425
  }
37603
38426
  if (attachPromptPaths) {
37604
- composerFeedback = "Attaching referenced files\u2026";
37605
- render();
38427
+ let handedOffToAdmission = false;
37606
38428
  try {
37607
38429
  const attachments = await attachPromptPaths({
37608
38430
  text: composerDraft.text,
37609
- excludedPaths: composerDraft.attachments.flatMap((attachment) => attachment.sourcePath ? [attachment.sourcePath] : [])
38431
+ excludedPaths: composerDraft.attachments.flatMap((attachment) => attachment.sourcePath ? [attachment.sourcePath] : []),
38432
+ selectedPaths: selectedAttachmentPaths({ text: composerDraft.text, selections: selectedPathCompletions }),
38433
+ onUploadStart: ({ filenames }) => {
38434
+ if (!ownsAttachmentPreparation(preparationOwner))
38435
+ return;
38436
+ attachmentFeedback = { kind: "progress", message: attachmentUploadMessage(filenames) };
38437
+ render();
38438
+ }
37610
38439
  });
38440
+ if (!ownsAttachmentPreparation(preparationOwner))
38441
+ return;
37611
38442
  composerDraft = { ...composerDraft, attachments: [...composerDraft.attachments, ...attachments] };
38443
+ selectedPathCompletions = [];
38444
+ attachmentFeedback = undefined;
38445
+ handedOffToAdmission = true;
37612
38446
  } catch (error93) {
37613
- composerFeedback = error93 instanceof Error ? error93.message : String(error93);
38447
+ if (!ownsAttachmentPreparation(preparationOwner))
38448
+ return;
38449
+ attachmentFeedback = { kind: "failure", message: error93 instanceof Error ? error93.message : String(error93) };
37614
38450
  render();
37615
38451
  return;
38452
+ } finally {
38453
+ if (!handedOffToAdmission && attachmentPreparationOwner === preparationOwner)
38454
+ invalidateAttachmentPreparation();
37616
38455
  }
37617
38456
  }
38457
+ if (!ownsAttachmentPreparation(preparationOwner))
38458
+ return;
38459
+ invalidateAttachmentPreparation();
37618
38460
  const submission = {
37619
38461
  id: crypto.randomUUID(),
37620
38462
  idempotencyKey: crypto.randomUUID(),
@@ -37651,13 +38493,13 @@ async function createSessionTui({
37651
38493
  if (command === "/complete") {
37652
38494
  composer.setText("");
37653
38495
  composerDraft = { ...composerDraft, text: "" };
37654
- await handleLifecycleRequest("complete");
38496
+ handleLifecycleRequest("complete");
37655
38497
  return true;
37656
38498
  }
37657
38499
  if (command === "/cancel") {
37658
38500
  composer.setText("");
37659
38501
  composerDraft = { ...composerDraft, text: "" };
37660
- await handleLifecycleRequest("cancel");
38502
+ handleLifecycleRequest("cancel");
37661
38503
  return true;
37662
38504
  }
37663
38505
  if (command === "/logout") {
@@ -37685,16 +38527,22 @@ async function createSessionTui({
37685
38527
  attachments: submission.attachments,
37686
38528
  idempotencyKey: submission.idempotencyKey
37687
38529
  });
38530
+ if (!viewIsActive())
38531
+ return;
37688
38532
  localPromptHistory.push(submission.text);
37689
38533
  const messageId = latestState.activeMessageId;
37690
38534
  admittedSubmissions = messageId ? admittedSubmissions.map((candidate) => candidate.id === submission.id ? { id: candidate.id, idempotencyKey: candidate.idempotencyKey, text: candidate.text, attachments: candidate.attachments, status: "submitted", messageId } : candidate) : admittedSubmissions.filter((candidate) => candidate.id !== submission.id);
37691
38535
  composerFeedback = undefined;
37692
38536
  } catch (error93) {
38537
+ if (!viewIsActive())
38538
+ return;
37693
38539
  admittedSubmissions = admittedSubmissions.map((candidate) => candidate.id === submission.id ? { id: candidate.id, idempotencyKey: candidate.idempotencyKey, text: candidate.text, attachments: candidate.attachments, status: "failed", error: error93 instanceof Error ? error93.message : String(error93) } : candidate);
37694
38540
  }
37695
38541
  render();
37696
38542
  }
37697
38543
  async function retryOldestFailedSubmission() {
38544
+ if (!viewIsActive())
38545
+ return;
37698
38546
  const failed = admittedSubmissions.find((submission) => submission.status === "failed");
37699
38547
  if (!failed)
37700
38548
  return;
@@ -37810,6 +38658,7 @@ async function createSessionTui({
37810
38658
  if (destroyed)
37811
38659
  return;
37812
38660
  destroyed = true;
38661
+ invalidateAttachmentPreparation();
37813
38662
  if (workingSpinnerTimer) {
37814
38663
  clearInterval(workingSpinnerTimer);
37815
38664
  workingSpinnerTimer = undefined;
@@ -37851,9 +38700,9 @@ function renderSlashCommandMenu({ commands, selectedIndex }) {
37851
38700
  const rows = commands.map((command, index) => {
37852
38701
  const selected = index === selectedIndex;
37853
38702
  const rowStyle = selected ? bg4(PALETTE.selectionBg) : (chunk) => chunk;
37854
- return new StyledText5([
37855
- rowStyle(fg6(PALETTE.remyAccent)(`${selected ? "\u203A " : " "}${command.value.padEnd(12)}`)),
37856
- rowStyle(fg6(PALETTE.dimText)(command.description))
38703
+ return new StyledText6([
38704
+ rowStyle(fg7(PALETTE.remyAccent)(`${selected ? "\u203A " : " "}${command.value.padEnd(12)}`)),
38705
+ rowStyle(fg7(PALETTE.dimText)(command.description))
37857
38706
  ]);
37858
38707
  });
37859
38708
  return joinStyled([joinStyled(rows, `
@@ -37874,15 +38723,15 @@ var helpEntries = [
37874
38723
  ];
37875
38724
  function renderHelpPanel() {
37876
38725
  const width = helpEntries.reduce((max, entry) => Math.max(max, entry.token.length), 0) + 2;
37877
- const rows = [new StyledText5([dim4(fg6(PALETTE.dimText)("What you can do here"))])];
38726
+ const rows = [new StyledText6([dim5(fg7(PALETTE.dimText)("What you can do here"))])];
37878
38727
  let lastGroup;
37879
38728
  for (const entry of helpEntries) {
37880
38729
  if (lastGroup !== undefined && entry.group !== lastGroup)
37881
- rows.push(new StyledText5([fg6(PALETTE.dimText)(" ")]));
38730
+ rows.push(new StyledText6([fg7(PALETTE.dimText)(" ")]));
37882
38731
  lastGroup = entry.group;
37883
- rows.push(new StyledText5([
37884
- fg6(PALETTE.remyAccent)(` ${entry.token.padEnd(width)}`),
37885
- dim4(fg6(PALETTE.dimText)(entry.description))
38732
+ rows.push(new StyledText6([
38733
+ fg7(PALETTE.remyAccent)(` ${entry.token.padEnd(width)}`),
38734
+ dim5(fg7(PALETTE.dimText)(entry.description))
37886
38735
  ]));
37887
38736
  }
37888
38737
  return joinStyled(rows, `
@@ -37896,23 +38745,22 @@ function renderStatusBand({ state, working, elapsedMs, stopState, lifecycleReque
37896
38745
  const pendingOperation = lifecycleRequestState?.kind === "pending" ? lifecycleRequestState.operation : undefined;
37897
38746
  const presentation = pendingOperation ? { label: lifecycleOperationPresentParticiple(pendingOperation), color: PALETTE.approvalQuestion } : stopping ? { label: "Stopping", color: PALETTE.approvalQuestion } : sessionViewStatePresentation({ aggregateStatus: state.aggregateStatus, isWorking: working });
37898
38747
  const elapsed = (working || stopping) && elapsedMs !== undefined && elapsedMs > 0 ? ` ${formatElapsed(elapsedMs)}` : "";
37899
- const connection = state.connectionStatus === "connected" ? dim4(fg6(PALETTE.dimText)("connected")) : fg6(PALETTE.approvalQuestion)("reconnecting\u2026");
37900
- const sessionLabel = state.sessionNumber !== undefined ? `Session #${state.sessionNumber}` : `Session ${state.sessionId}`;
37901
- const context = renderContextStatus(state.context.snapshot);
38748
+ const connection = state.connectionStatus === "connected" ? new StyledText6([fg7(PALETTE.statusCompleted)("\u25CF "), dim5(fg7(PALETTE.dimText)("connected"))]) : new StyledText6([fg7(PALETTE.approvalQuestion)("\u25CF reconnecting\u2026")]);
38749
+ const sessionLabel = state.sessionNumber !== undefined ? `#${state.sessionNumber}` : state.sessionId;
37902
38750
  const pullRequest = state.pullRequest;
37903
- return new StyledText5([
37904
- fg6(presentation.color)("\u25CF "),
37905
- ...repositoryLabel ? [dim4(fg6(PALETTE.dimText)(`${repositoryLabel} \xB7 `))] : [],
37906
- dim4(fg6(PALETTE.dimText)(`${sessionLabel} \xB7 `)),
37907
- bold4(fg6(presentation.color)(`${presentation.label}${elapsed}`)),
37908
- dim4(fg6(PALETTE.dimText)(" \xB7 ")),
37909
- connection,
37910
- ...context ? [dim4(fg6(PALETTE.dimText)(` \xB7 ${context}`))] : [],
37911
- ...pullRequest ? [
37912
- dim4(fg6(PALETTE.dimText)(" \xB7 ")),
37913
- bold4(fg6(statusColor(pullRequest.status))(`PR #${pullRequest.number} \xB7 ${pullRequest.draft ? "Draft" : sessionStatusLabel(pullRequest.status)}`))
37914
- ] : []
37915
- ]);
38751
+ const contextStatus = renderContextStatus(state.context.snapshot);
38752
+ const headerContext = [
38753
+ ...repositoryLabel ? [[dim5(fg7(PALETTE.dimText)(repositoryLabel))]] : [],
38754
+ ...pullRequest ? [[bold4(fg7(statusColor(pullRequest.status))(`PR#${pullRequest.number}`))]] : [],
38755
+ [bold4(fg7(presentation.color)(`${presentation.label}${elapsed}`))],
38756
+ connection.chunks,
38757
+ ...contextStatus ? [[dim5(fg7(PALETTE.dimText)(contextStatus))]] : []
38758
+ ];
38759
+ return joinStyled([
38760
+ [bold4(fg7(PALETTE.bodyText)(`${sessionLabel} ${state.title ?? "Untitled session"}`))],
38761
+ joinStyled(headerContext, " \xB7 ")
38762
+ ], `
38763
+ `);
37916
38764
  }
37917
38765
  function renderContextStatus(snapshot) {
37918
38766
  if (snapshot.status === "unknown")
@@ -37923,33 +38771,39 @@ function renderContextStatus(snapshot) {
37923
38771
  function renderTerminalSessionBand({ aggregateStatus }) {
37924
38772
  const status = sessionStatusLabel(aggregateStatus).toLowerCase();
37925
38773
  return joinStyled([
37926
- new StyledText5([
37927
- bg4(PALETTE.selectionBg)(bold4(fg6(PALETTE.systemAccent)(` Session ${status} `))),
37928
- bg4(PALETTE.selectionBg)(fg6(PALETTE.bodyText)("\u2014 this conversation is closed. "))
38774
+ new StyledText6([
38775
+ bg4(PALETTE.selectionBg)(bold4(fg7(PALETTE.systemAccent)(` Session ${status} `))),
38776
+ bg4(PALETTE.selectionBg)(fg7(PALETTE.bodyText)("\u2014 this conversation is closed. "))
37929
38777
  ]),
37930
- new StyledText5([
37931
- bg4(PALETTE.selectionBg)(fg6(PALETTE.remyAccent)(" esc: ")),
37932
- bg4(PALETTE.selectionBg)(dim4(fg6(PALETTE.dimText)("return to dashboard \xB7 "))),
37933
- bg4(PALETTE.selectionBg)(fg6(PALETTE.remyAccent)("n: ")),
37934
- bg4(PALETTE.selectionBg)(dim4(fg6(PALETTE.dimText)("new session ")))
38778
+ new StyledText6([
38779
+ bg4(PALETTE.selectionBg)(fg7(PALETTE.remyAccent)(" esc: ")),
38780
+ bg4(PALETTE.selectionBg)(dim5(fg7(PALETTE.dimText)("return to dashboard \xB7 "))),
38781
+ bg4(PALETTE.selectionBg)(fg7(PALETTE.remyAccent)("n: ")),
38782
+ bg4(PALETTE.selectionBg)(dim5(fg7(PALETTE.dimText)("new session ")))
37935
38783
  ])
37936
38784
  ], `
37937
38785
  `);
37938
38786
  }
37939
38787
  function renderActionBar({ state, working, stopState, lifecycleRequestState }) {
37940
38788
  if (lifecycleRequestState.kind === "pending")
37941
- return new StyledText5([dim4(fg6(PALETTE.dimText)(`${lifecycleOperationPresentParticiple(lifecycleRequestState.operation).toLowerCase()} the session\u2026`))]);
38789
+ return new StyledText6([dim5(fg7(PALETTE.dimText)(`${lifecycleOperationPresentParticiple(lifecycleRequestState.operation).toLowerCase()} the session\u2026`))]);
37942
38790
  if (state.aggregateStatus !== "open")
37943
- return new StyledText5([]);
38791
+ return new StyledText6([]);
37944
38792
  const stopToken = working && stopState.kind === "failed" ? ["esc retry stop"] : [];
37945
38793
  const idleGroup = lifecycleRequestState.kind === "failed" ? `/${lifecycleRequestState.operation} retries \xB7 /new \xB7 /sessions` : "/new \xB7 /sessions \xB7 /complete \xB7 /cancel";
37946
38794
  const tokens = working ? ["/ commands", ...stopToken, "ctrl+o activity"] : ["/ commands", idleGroup, "ctrl+o activity"];
37947
- return new StyledText5([dim4(fg6(PALETTE.dimText)(tokens.join(" \xB7 ")))]);
38795
+ return new StyledText6([dim5(fg7(PALETTE.dimText)(tokens.join(" \xB7 ")))]);
37948
38796
  }
37949
38797
  function isRemyWorking(state) {
37950
- if (state.aggregateStatus !== "open" || state.connectionStatus !== "connected")
38798
+ if (state.aggregateStatus !== "open" || state.connectionStatus !== "connected") {
37951
38799
  return false;
37952
- return hasRunningTurn(state) || state.activeMessageId !== undefined && state.messageTurns[state.activeMessageId]?.outcome === undefined;
38800
+ }
38801
+ if (state.activeMessageId !== undefined && state.messageTurns[state.activeMessageId]?.outcome === undefined)
38802
+ return true;
38803
+ return isWorkingAgentStatus(state.agentStatus) && hasRunningTurn(state);
38804
+ }
38805
+ function isWorkingAgentStatus(agentStatus) {
38806
+ return agentStatus === undefined || agentStatus === "pending" || agentStatus === "queued" || agentStatus === "running";
37953
38807
  }
37954
38808
  function workingTurnStartedAt(state) {
37955
38809
  const activeTurn = state.activeMessageId === undefined ? undefined : state.messageTurns[state.activeMessageId];
@@ -37990,58 +38844,64 @@ function renderTimeline({ state, activityExpanded }) {
37990
38844
  }
37991
38845
  function renderTimelineItem(item, activityExpanded) {
37992
38846
  if (item.kind === "message") {
37993
- const header = new StyledText5([
38847
+ const header = new StyledText6([
37994
38848
  renderTimestampChunk(item.occurredAt),
37995
38849
  renderRoleLabel({ label: item.author.label, role: item.author.role }),
37996
- ...item.author.source ? [dim4(fg6(PALETTE.dimText)(` (${item.author.source})`))] : []
38850
+ ...item.author.source ? [dim5(fg7(PALETTE.dimText)(` (${item.author.source})`))] : []
37997
38851
  ]);
37998
- return joinStyled([header, renderMessageBody({ text: item.text, role: item.author.role })], `
38852
+ const attachments = renderAttachmentSummary(item.attachments);
38853
+ return joinStyled([
38854
+ header,
38855
+ renderMessageBody({ text: item.text, role: item.author.role }),
38856
+ ...attachments.chunks.length > 0 ? [attachments] : []
38857
+ ], `
37999
38858
  `);
38000
38859
  }
38001
38860
  if (item.kind === "plan")
38002
38861
  return renderPlan({ item, expanded: activityExpanded });
38003
38862
  if (item.artifactKind === "tela_page") {
38004
- return new StyledText5([
38863
+ return new StyledText6([
38005
38864
  renderTimestampChunk(item.occurredAt),
38006
- bold4(fg6(PALETTE.tool)("Tela Page: ")),
38007
- fg6(PALETTE.bodyText)(item.title),
38008
- dim4(fg6(PALETTE.dimText)(` \xB7 ${item.url}`))
38865
+ bold4(fg7(PALETTE.tool)("Tela Page: ")),
38866
+ fg7(PALETTE.bodyText)(item.title),
38867
+ dim5(fg7(PALETTE.dimText)(` \xB7 ${item.url}`))
38009
38868
  ]);
38010
38869
  }
38011
- return new StyledText5([
38870
+ return new StyledText6([
38012
38871
  renderTimestampChunk(item.occurredAt),
38013
- bold4(fg6(PALETTE.tool)("Artifact: ")),
38014
- fg6(PALETTE.bodyText)(item.title)
38872
+ bold4(fg7(PALETTE.tool)("Artifact: ")),
38873
+ fg7(PALETTE.bodyText)(item.title),
38874
+ ...item.previewUrl ? [dim5(fg7(PALETTE.dimText)(` (${item.previewUrl})`))] : []
38015
38875
  ]);
38016
38876
  }
38017
38877
  var collapsedPlanItemLimit = 5;
38018
38878
  function renderPlan({ item, expanded }) {
38019
38879
  if (item.items.length === 0)
38020
- return new StyledText5([]);
38880
+ return new StyledText6([]);
38021
38881
  const completed = item.items.filter((workItem) => workItem.status === "completed").length;
38022
38882
  const visibleItems = expanded ? item.items : collapsedPlanItems(item.items);
38023
38883
  const rows = visibleItems.map((workItem) => {
38024
38884
  if (workItem.status === "in_progress") {
38025
- return new StyledText5([
38026
- fg6(PALETTE.remyAccent)(" \u25B8 "),
38027
- bold4(fg6(PALETTE.bodyText)(workItem.title))
38885
+ return new StyledText6([
38886
+ fg7(PALETTE.remyAccent)(" \u25B8 "),
38887
+ bold4(fg7(PALETTE.bodyText)(workItem.title))
38028
38888
  ]);
38029
38889
  }
38030
38890
  const glyph = workItem.status === "completed" ? "\u2713" : "\u25CB";
38031
- return new StyledText5([
38032
- fg6(PALETTE.dimText)(` ${glyph} `),
38033
- dim4(fg6(PALETTE.dimText)(workItem.title))
38891
+ return new StyledText6([
38892
+ fg7(PALETTE.dimText)(` ${glyph} `),
38893
+ dim5(fg7(PALETTE.dimText)(workItem.title))
38034
38894
  ]);
38035
38895
  });
38036
38896
  const hidden = item.items.length - visibleItems.length;
38037
38897
  return joinStyled([
38038
- new StyledText5([
38898
+ new StyledText6([
38039
38899
  renderTimestampChunk(item.occurredAt),
38040
- bold4(fg6(PALETTE.remyAccent)("Plan")),
38041
- dim4(fg6(PALETTE.dimText)(` \xB7 ${completed}/${item.items.length} done`))
38900
+ bold4(fg7(PALETTE.remyAccent)("Plan")),
38901
+ dim5(fg7(PALETTE.dimText)(` \xB7 ${completed}/${item.items.length} done`))
38042
38902
  ]),
38043
38903
  ...rows,
38044
- ...hidden > 0 ? [new StyledText5([dim4(fg6(PALETTE.dimText)(` \u2026 ${hidden} more task${hidden === 1 ? "" : "s"} \xB7 Ctrl+O details`))])] : []
38904
+ ...hidden > 0 ? [new StyledText6([dim5(fg7(PALETTE.dimText)(` \u2026 ${hidden} more task${hidden === 1 ? "" : "s"} \xB7 Ctrl+O details`))])] : []
38045
38905
  ], `
38046
38906
  `);
38047
38907
  }
@@ -38088,7 +38948,7 @@ function canonicalValuesEqual(left, right) {
38088
38948
  }
38089
38949
  function renderActivityGroup({ items, activityExpanded }) {
38090
38950
  if (items.length === 0)
38091
- return new StyledText5([]);
38951
+ return new StyledText6([]);
38092
38952
  const summary = renderActivitySummary({ items, includeTimestamp: activityExpanded });
38093
38953
  const lastInFlightIndex = items.length - 1;
38094
38954
  if (!activityExpanded) {
@@ -38097,9 +38957,9 @@ function renderActivityGroup({ items, activityExpanded }) {
38097
38957
  item,
38098
38958
  inFlight: item.card.kind === "tool" && tailStart + index === lastInFlightIndex
38099
38959
  }));
38100
- const elision = tailStart > 0 ? new StyledText5([dim4(fg6(PALETTE.dimText)(` \u2026 ${tailStart} earlier step${tailStart === 1 ? "" : "s"}`))]) : undefined;
38960
+ const elision = tailStart > 0 ? new StyledText6([dim5(fg7(PALETTE.dimText)(` \u2026 ${tailStart} earlier step${tailStart === 1 ? "" : "s"}`))]) : undefined;
38101
38961
  return joinStyled([
38102
- new StyledText5([renderTimestampChunk(items[0].occurredAt), dim4(fg6(PALETTE.dimText)("Worked"))]),
38962
+ new StyledText6([renderTimestampChunk(items[0].occurredAt), dim5(fg7(PALETTE.dimText)("Worked"))]),
38103
38963
  ...elision ? [elision] : [],
38104
38964
  ...tail,
38105
38965
  summary
@@ -38118,9 +38978,9 @@ function renderExpandedActivityHierarchy({ items, lastInFlightIndex }) {
38118
38978
  const attribution = item.card.attribution;
38119
38979
  if (attribution?.status === "resolved") {
38120
38980
  if (activeOwnerKey !== attribution.ownerKey) {
38121
- rendered.push(new StyledText5([
38122
- dim4(fg6(PALETTE.dimText)(" ")),
38123
- fg6(PALETTE.bodyText)(`Subagent \xB7 ${lineagePathLabel(attribution.path)}`)
38981
+ rendered.push(new StyledText6([
38982
+ dim5(fg7(PALETTE.dimText)(" ")),
38983
+ fg7(PALETTE.bodyText)(`Subagent \xB7 ${lineagePathLabel(attribution.path)}`)
38124
38984
  ]));
38125
38985
  }
38126
38986
  activeOwnerKey = attribution.ownerKey;
@@ -38147,11 +39007,11 @@ function renderExpandedActivityStep({ item, count, inFlight, nested }) {
38147
39007
  const disclosure = renderActivityDisclosure(item.card);
38148
39008
  const title = expandedActivityTitle(item.card);
38149
39009
  const indent = nested ? " " : " ";
38150
- const step = new StyledText5([
38151
- fg6(color)(`${indent}${glyph} `),
38152
- fg6(PALETTE.bodyText)(title),
38153
- ...count > 1 ? [dim4(fg6(PALETTE.dimText)(` \xD7${count}`))] : [],
38154
- ...showSummary ? [dim4(fg6(PALETTE.dimText)(`
39010
+ const step = new StyledText6([
39011
+ fg7(color)(`${indent}${glyph} `),
39012
+ fg7(PALETTE.bodyText)(title),
39013
+ ...count > 1 ? [dim5(fg7(PALETTE.dimText)(` \xD7${count}`))] : [],
39014
+ ...showSummary ? [dim5(fg7(PALETTE.dimText)(`
38155
39015
  ${indent} ${item.card.summary}`))] : []
38156
39016
  ]);
38157
39017
  return disclosure ? joinStyled([step, disclosure], `
@@ -38178,23 +39038,23 @@ function renderActivityDisclosure(card) {
38178
39038
  const details = [];
38179
39039
  if (card.detail !== undefined) {
38180
39040
  const safeDetail = terminalSafeText(card.detail);
38181
- details.push(card.detailFormat === "code" ? renderCodeSnippet({ text: safeDetail, indent: " " }) : new StyledText5([dim4(fg6(PALETTE.dimText)(` ${safeDetail}`))]));
39041
+ details.push(card.detailFormat === "code" ? renderCodeSnippet({ text: safeDetail, indent: " " }) : new StyledText6([dim5(fg7(PALETTE.dimText)(` ${safeDetail}`))]));
38182
39042
  }
38183
39043
  return details.length === 0 ? undefined : joinStyled(details, `
38184
39044
 
38185
39045
  `);
38186
39046
  }
38187
39047
  function renderActivitySummary({ items, includeTimestamp }) {
38188
- return new StyledText5([
39048
+ return new StyledText6([
38189
39049
  ...includeTimestamp ? [renderTimestampChunk(items[0].occurredAt)] : [],
38190
- dim4(fg6(PALETTE.dimText)(`Worked \xB7 ${items.length} step${items.length === 1 ? "" : "s"} \xB7 Ctrl+O details`))
39050
+ dim5(fg7(PALETTE.dimText)(`Worked \xB7 ${items.length} step${items.length === 1 ? "" : "s"} \xB7 Ctrl+O details`))
38191
39051
  ]);
38192
39052
  }
38193
39053
  function renderCollapsedActivityStep({ item, inFlight }) {
38194
39054
  const { glyph, color } = activityGlyph({ kind: item.card.kind, inFlight });
38195
- return new StyledText5([
38196
- fg6(color)(` ${glyph} `),
38197
- dim4(fg6(PALETTE.dimText)(collapsedActivityTitle(item.card)))
39055
+ return new StyledText6([
39056
+ fg7(color)(` ${glyph} `),
39057
+ dim5(fg7(PALETTE.dimText)(collapsedActivityTitle(item.card)))
38198
39058
  ]);
38199
39059
  }
38200
39060
  function collapsedActivityTitle(card) {
@@ -38211,15 +39071,15 @@ function collapsedActivityTitle(card) {
38211
39071
  function renderSubmittedTimeline({ state, admittedSubmissions }) {
38212
39072
  const submitted = admittedSubmissions.filter((submission) => submission.status === "submitted" && !state.transcript.some((item) => item.kind === "message" && item.messageId === submission.messageId));
38213
39073
  const messages = submitted.map((submission) => {
38214
- const attachments = submission.attachments.length > 0 ? dim4(fg6(PALETTE.dimText)(`Attachments: ${submission.attachments.map((attachment) => attachment.filename).join(", ")}`)) : undefined;
38215
- const header = new StyledText5([
39074
+ const attachments = renderAttachmentSummary(submission.attachments);
39075
+ const header = new StyledText6([
38216
39076
  renderRoleLabel({ label: "You", role: "human" }),
38217
- dim4(fg6(PALETTE.dimText)(" (CLI)"))
39077
+ dim5(fg7(PALETTE.dimText)(" (CLI)"))
38218
39078
  ]);
38219
39079
  return joinStyled([
38220
39080
  header,
38221
39081
  renderMessageBody({ text: submission.text, role: "human" }),
38222
- ...attachments ? [new StyledText5([attachments])] : []
39082
+ ...attachments.chunks.length > 0 ? [attachments] : []
38223
39083
  ], `
38224
39084
  `);
38225
39085
  });
@@ -38229,20 +39089,20 @@ function renderSubmittedTimeline({ state, admittedSubmissions }) {
38229
39089
  }
38230
39090
  function renderWorkingIndicator({ frame, elapsedMs, mode }) {
38231
39091
  if (mode === "stopping") {
38232
- return new StyledText5([
38233
- fg6(PALETTE.approvalQuestion)("\u23F9"),
38234
- dim4(fg6(PALETTE.dimText)(` Stopping\u2026 ${formatElapsed(elapsedMs)}`))
39092
+ return new StyledText6([
39093
+ fg7(PALETTE.approvalQuestion)("\u23F9"),
39094
+ dim5(fg7(PALETTE.dimText)(` Stopping\u2026 ${formatElapsed(elapsedMs)}`))
38235
39095
  ]);
38236
39096
  }
38237
39097
  if (mode === "complete" || mode === "cancel") {
38238
- return new StyledText5([
38239
- fg6(PALETTE.approvalQuestion)(frame),
38240
- dim4(fg6(PALETTE.dimText)(` ${lifecycleOperationPresentParticiple(mode)} the session\u2026`))
39098
+ return new StyledText6([
39099
+ fg7(PALETTE.approvalQuestion)(frame),
39100
+ dim5(fg7(PALETTE.dimText)(` ${lifecycleOperationPresentParticiple(mode)} the session\u2026`))
38241
39101
  ]);
38242
39102
  }
38243
- return new StyledText5([
38244
- fg6(PALETTE.remyAccent)(frame),
38245
- dim4(fg6(PALETTE.dimText)(` Working ${formatElapsed(elapsedMs)}`))
39103
+ return new StyledText6([
39104
+ fg7(PALETTE.remyAccent)(frame),
39105
+ dim5(fg7(PALETTE.dimText)(` Working ${formatElapsed(elapsedMs)}`))
38246
39106
  ]);
38247
39107
  }
38248
39108
  function lifecycleOperationPresentParticiple(operation) {
@@ -38264,22 +39124,22 @@ function truncate2(value, width) {
38264
39124
  }
38265
39125
  function renderComposerStatus({ admittedSubmissions }) {
38266
39126
  if (admittedSubmissions.length === 0)
38267
- return new StyledText5([]);
39127
+ return new StyledText6([]);
38268
39128
  const parts = admittedSubmissions.flatMap((submission) => {
38269
39129
  if (submission.status === "submitted")
38270
39130
  return [];
38271
- const attachments = submission.attachments.length > 0 ? `
38272
- Attachments: ${submission.attachments.map((attachment) => attachment.filename).join(", ")}` : "";
38273
- return [submission.status === "failed" ? new StyledText5([
38274
- bold4(fg6(PALETTE.failure)("Failed: ")),
38275
- fg6(PALETTE.bodyText)(`${submission.error ?? ""}
38276
- ${submission.text}${attachments}
38277
- `),
38278
- dim4(fg6(PALETTE.dimText)("Ctrl+R retries oldest failed submission."))
38279
- ]) : new StyledText5([
38280
- fg6(PALETTE.progress)("Admitting: "),
38281
- fg6(PALETTE.bodyText)(`${submission.text}${attachments}`)
38282
- ])];
39131
+ const attachments = renderAttachmentSummary(submission.attachments);
39132
+ return [submission.status === "failed" ? joinStyled([
39133
+ new StyledText6([bold4(fg7(PALETTE.failure)("Failed: ")), fg7(PALETTE.bodyText)(submission.error ?? "")]),
39134
+ renderMessageBody({ text: submission.text, role: "human" }),
39135
+ ...attachments.chunks.length > 0 ? [attachments] : [],
39136
+ new StyledText6([dim5(fg7(PALETTE.dimText)("Ctrl+R retries oldest failed submission."))])
39137
+ ], `
39138
+ `) : joinStyled([
39139
+ new StyledText6([fg7(PALETTE.progress)("Admitting: "), fg7(PALETTE.bodyText)(submission.text)]),
39140
+ ...attachments.chunks.length > 0 ? [attachments] : []
39141
+ ], `
39142
+ `)];
38283
39143
  });
38284
39144
  return joinStyled(parts, `
38285
39145
 
@@ -38290,10 +39150,15 @@ async function createDefaultRenderer3() {
38290
39150
  }
38291
39151
 
38292
39152
  // src/tui/attachments.ts
39153
+ import { Buffer as Buffer2 } from "buffer";
38293
39154
  import { randomUUID as randomUUID4 } from "crypto";
38294
- import { writeFile as writeFile3, stat } from "fs/promises";
38295
- import { homedir as homedir2 } from "os";
39155
+ import { mkdtemp, open as open4, readdir as readdir2, rm, stat, writeFile as writeFile3 } from "fs/promises";
39156
+ import { homedir as homedir2, tmpdir } from "os";
38296
39157
  import { basename as basename3, isAbsolute as isAbsolute2, join as join5, resolve as resolve2 } from "path";
39158
+ var directoryArchiveMediaType = "application/gzip";
39159
+ var directoryArchiveSuffix = ".tar.gz";
39160
+ var maxPortableFilenameBytes = 255;
39161
+ var attachmentSizeLimitLabel = `${fileUploadMaxBytes / (1024 * 1024)} MiB`;
38297
39162
  async function readClipboardImage() {
38298
39163
  if (process.platform !== "darwin")
38299
39164
  throw new Error("Clipboard image paste is supported on macOS only.");
@@ -38319,11 +39184,13 @@ async function attachPromptPaths({
38319
39184
  text,
38320
39185
  cwd,
38321
39186
  excludedPaths = [],
38322
- reserveAndUploadFile: reserveAndUploadFile2
39187
+ selectedPaths = [],
39188
+ reserveAndUploadFile: reserveAndUploadFile2,
39189
+ onUploadStart
38323
39190
  }) {
38324
39191
  const excluded = new Set(excludedPaths);
38325
- const attachments = [];
38326
- for (const candidate of promptFilePaths({ text, cwd })) {
39192
+ const pathsToUpload = [];
39193
+ for (const candidate of promptFilePaths({ text, cwd, selectedPaths })) {
38327
39194
  const { filePath } = candidate;
38328
39195
  if (excluded.has(filePath))
38329
39196
  continue;
@@ -38335,27 +39202,168 @@ async function attachPromptPaths({
38335
39202
  continue;
38336
39203
  throw error93;
38337
39204
  }
38338
- if (!file3.isFile() && candidate.explicit)
38339
- throw new Error(`Prompt attachment path is not a regular file: ${filePath}`);
38340
- if (!file3.isFile())
39205
+ if (!file3.isFile() && !file3.isDirectory() && candidate.explicit)
39206
+ throw new Error(`Prompt attachment path is not a regular file or directory: ${filePath}`);
39207
+ if (!file3.isFile() && !file3.isDirectory())
38341
39208
  continue;
39209
+ pathsToUpload.push(filePath);
39210
+ }
39211
+ if (pathsToUpload.length === 0)
39212
+ return [];
39213
+ onUploadStart?.({ filenames: pathsToUpload.map((filePath) => basename3(filePath)) });
39214
+ const attachments = [];
39215
+ for (const filePath of pathsToUpload) {
39216
+ const uploaded = await uploadLocalAttachment({ filePath, reserveAndUploadFile: reserveAndUploadFile2 });
38342
39217
  attachments.push({
38343
39218
  id: randomUUID4(),
38344
- fileId: await reserveAndUploadFile2({ filePath }),
38345
- filename: basename3(filePath),
38346
- sourcePath: filePath
39219
+ ...uploaded
38347
39220
  });
38348
39221
  }
38349
39222
  return attachments;
38350
39223
  }
38351
- function promptFilePaths({ text, cwd }) {
39224
+ async function uploadLocalAttachment({
39225
+ filePath,
39226
+ reserveAndUploadFile: reserveAndUploadFile2
39227
+ }) {
39228
+ const sourcePath = resolve2(filePath);
39229
+ const source = await stat(sourcePath);
39230
+ if (source.isFile()) {
39231
+ if (source.size > fileUploadMaxBytes)
39232
+ throw oversizedFileAttachmentError(sourcePath);
39233
+ return {
39234
+ fileId: await reserveAndUploadFile2({ filePath: sourcePath }),
39235
+ filename: basename3(sourcePath),
39236
+ sourcePath
39237
+ };
39238
+ }
39239
+ if (!source.isDirectory())
39240
+ throw new Error(`Attachment path is not a regular file or directory: ${sourcePath}`);
39241
+ const archive = await createDirectoryArchive(sourcePath);
39242
+ try {
39243
+ return {
39244
+ fileId: await reserveAndUploadFile2({ filePath: archive.path, mediaType: directoryArchiveMediaType }),
39245
+ filename: archive.filename,
39246
+ sourcePath
39247
+ };
39248
+ } finally {
39249
+ await rm(archive.temporaryDirectory, { recursive: true, force: true });
39250
+ }
39251
+ }
39252
+ async function createDirectoryArchive(directoryPath) {
39253
+ const rootName = basename3(directoryPath) || "directory";
39254
+ const files = Object.create(null);
39255
+ await collectDirectoryFiles({
39256
+ directoryPath,
39257
+ archivePath: rootName,
39258
+ attachmentDirectoryPath: directoryPath,
39259
+ collectedByteSize: 0,
39260
+ files
39261
+ });
39262
+ if (Object.keys(files).length === 0)
39263
+ throw new Error(`Directory attachment contains no regular files: ${directoryPath}`);
39264
+ const temporaryDirectory = await mkdtemp(join5(tmpdir(), "remy-directory-attachment-"));
39265
+ const filename = directoryArchiveFilename(rootName);
39266
+ const archivePath = join5(temporaryDirectory, filename);
39267
+ try {
39268
+ await Bun.Archive.write(archivePath, files, { compress: "gzip" });
39269
+ if ((await stat(archivePath)).size > fileUploadMaxBytes)
39270
+ throw oversizedDirectoryArchiveError(directoryPath);
39271
+ } catch (error93) {
39272
+ await rm(temporaryDirectory, { recursive: true, force: true });
39273
+ throw error93;
39274
+ }
39275
+ return { path: archivePath, filename, temporaryDirectory };
39276
+ }
39277
+ async function collectDirectoryFiles({
39278
+ directoryPath,
39279
+ archivePath,
39280
+ attachmentDirectoryPath,
39281
+ collectedByteSize,
39282
+ files
39283
+ }) {
39284
+ const entries = await readdir2(directoryPath, { withFileTypes: true });
39285
+ entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
39286
+ let byteSize = collectedByteSize;
39287
+ for (const entry of entries) {
39288
+ const entryPath = join5(directoryPath, entry.name);
39289
+ const entryArchivePath = `${archivePath}/${entry.name}`;
39290
+ if (entry.isDirectory()) {
39291
+ byteSize = await collectDirectoryFiles({
39292
+ directoryPath: entryPath,
39293
+ archivePath: entryArchivePath,
39294
+ attachmentDirectoryPath,
39295
+ collectedByteSize: byteSize,
39296
+ files
39297
+ });
39298
+ continue;
39299
+ }
39300
+ if (entry.isFile()) {
39301
+ const fileHandle = await open4(entryPath, "r");
39302
+ try {
39303
+ const entryStat = await fileHandle.stat();
39304
+ if (!entryStat.isFile())
39305
+ throw new Error(`Directory attachment contains a non-regular entry: ${entryPath}`);
39306
+ const remainingBytes = fileUploadMaxBytes - byteSize;
39307
+ if (entryStat.size > remainingBytes)
39308
+ throw oversizedDirectoryAttachmentError(attachmentDirectoryPath);
39309
+ const readResult = await readFileHandleWithLimit({
39310
+ fileHandle,
39311
+ initialByteSize: entryStat.size,
39312
+ maxBytes: remainingBytes
39313
+ });
39314
+ if (readResult.status === "limit_exceeded")
39315
+ throw oversizedDirectoryAttachmentError(attachmentDirectoryPath);
39316
+ files[entryArchivePath] = readResult.bytes;
39317
+ byteSize += readResult.bytes.byteLength;
39318
+ } finally {
39319
+ await fileHandle.close();
39320
+ }
39321
+ continue;
39322
+ }
39323
+ if (entry.isSymbolicLink())
39324
+ throw new Error(`Directory attachment contains a symbolic link: ${entryPath}`);
39325
+ throw new Error(`Directory attachment contains a non-regular entry: ${entryPath}`);
39326
+ }
39327
+ return byteSize;
39328
+ }
39329
+ function oversizedFileAttachmentError(filePath) {
39330
+ return new Error(`Attachment file ${JSON.stringify(filePath)} is larger than ${attachmentSizeLimitLabel}. Choose a file no larger than ${attachmentSizeLimitLabel}.`);
39331
+ }
39332
+ function oversizedDirectoryAttachmentError(directoryPath) {
39333
+ return new Error(`Attachment directory ${JSON.stringify(directoryPath)} contains more than ${attachmentSizeLimitLabel} of files. Choose fewer or smaller files.`);
39334
+ }
39335
+ function oversizedDirectoryArchiveError(directoryPath) {
39336
+ return new Error(`Attachment directory ${JSON.stringify(directoryPath)} produces an archive larger than ${attachmentSizeLimitLabel}. Choose fewer or smaller files.`);
39337
+ }
39338
+ function directoryArchiveFilename(directoryName) {
39339
+ const stem = truncateUtf8(directoryName, maxPortableFilenameBytes - Buffer2.byteLength(directoryArchiveSuffix)) || "directory";
39340
+ return `${stem}${directoryArchiveSuffix}`;
39341
+ }
39342
+ function truncateUtf8(value, maxBytes) {
39343
+ let result = "";
39344
+ let resultBytes = 0;
39345
+ for (const character of value) {
39346
+ const characterBytes = Buffer2.byteLength(character);
39347
+ if (resultBytes + characterBytes > maxBytes)
39348
+ break;
39349
+ result += character;
39350
+ resultBytes += characterBytes;
39351
+ }
39352
+ return result;
39353
+ }
39354
+ function promptFilePaths({ text, cwd, selectedPaths }) {
38352
39355
  const paths = new Map;
39356
+ for (const selectedPath of selectedPaths)
39357
+ paths.set(isAbsolute2(selectedPath) ? selectedPath : resolve2(cwd, selectedPath), true);
38353
39358
  for (const rawToken of text.split(/\s+/)) {
38354
39359
  const token = rawToken.replace(/^[([{'"`]+/, "").replace(/[),.;:!?\]}"`]+$/, "");
38355
39360
  const pathText = token.startsWith("@") ? token.slice(1) : token;
38356
39361
  if (!pathText)
38357
39362
  continue;
38358
39363
  const explicit = token.startsWith("@") || pathText.startsWith("./") || pathText.startsWith("../") || isAbsolute2(pathText) || pathText.startsWith("~/");
39364
+ const pathShaped = explicit || pathText.includes("/") || pathText.includes("\\") || pathText.includes(".");
39365
+ if (!pathShaped)
39366
+ continue;
38359
39367
  const expandedPath = pathText.startsWith("~/") ? join5(homedir2(), pathText.slice(2)) : pathText;
38360
39368
  const filePath = isAbsolute2(expandedPath) ? expandedPath : resolve2(cwd, expandedPath);
38361
39369
  paths.set(filePath, paths.get(filePath) || explicit);
@@ -38378,7 +39386,7 @@ function isPng(bytes) {
38378
39386
  }
38379
39387
 
38380
39388
  // src/tui/remy-splash.ts
38381
- import { bold as bold5, BoxRenderable as BoxRenderable5, fg as fg7, NativeImage, StyledText as StyledText6, TextRenderable as TextRenderable5, bg as bg5 } from "@opentui/core";
39389
+ import { bold as bold5, BoxRenderable as BoxRenderable5, fg as fg8, NativeImage, StyledText as StyledText7, TextRenderable as TextRenderable5, bg as bg5 } from "@opentui/core";
38382
39390
  // src/tui/remy-mark.ts
38383
39391
  var remyPixelFieldSource = new URL("../assets/remy-pixel-field.png", import.meta.url);
38384
39392
  function shouldShowRemySplash({ width, height }) {
@@ -38397,7 +39405,7 @@ var compactMarkRows = 9;
38397
39405
  var compactMinWidth = 48;
38398
39406
  var compactMinHeight = 20;
38399
39407
  var markBrightnessGain = 4.2;
38400
- var remyCliVersion = "1.13.0";
39408
+ var remyCliVersion = "1.14.0";
38401
39409
  async function showRemySplash({
38402
39410
  createRenderer = createRemyRenderer,
38403
39411
  durationMs = splashDurationMs,
@@ -38455,7 +39463,7 @@ async function showRemySplash({
38455
39463
  });
38456
39464
  const approvalCopy = new TextRenderable5(renderer, { content: "" });
38457
39465
  const metadata = new TextRenderable5(renderer, {
38458
- content: new StyledText6([fg7(PALETTE.dimText)(`v${remyCliVersion} \xB7 Tela\xAE`)])
39466
+ content: new StyledText7([fg8(PALETTE.dimText)(`v${remyCliVersion} \xB7 Tela\xAE`)])
38459
39467
  });
38460
39468
  const field = await loadPixelFieldUntilAbort({ signal });
38461
39469
  if (!field || signal?.aborted)
@@ -38553,32 +39561,32 @@ function createSplashController({
38553
39561
  };
38554
39562
  }
38555
39563
  function renderBootstrapActions({ state, frame }) {
38556
- return new StyledText6([
39564
+ return new StyledText7([
38557
39565
  actionText({ label: state.authenticatedEmail ? `Authenticated as ${state.authenticatedEmail}` : "Checking authentication...", status: state.authentication, frame }),
38558
- fg7(PALETTE.dimText)(`
39566
+ fg8(PALETTE.dimText)(`
38559
39567
  `),
38560
39568
  actionText({ label: "Load sessions", status: state.loadingSessions, frame })
38561
39569
  ]);
38562
39570
  }
38563
39571
  function actionText({ label, status, frame }) {
38564
39572
  if (status === "complete")
38565
- return fg7(PALETTE.statusCompleted)(`\u2713 ${label === "Load sessions" ? "Sessions loaded!" : label}`);
39573
+ return fg8(PALETTE.statusCompleted)(`\u2713 ${label === "Load sessions" ? "Sessions loaded!" : label}`);
38566
39574
  if (status === "needed")
38567
- return fg7(PALETTE.approvalQuestion)("\u26A0 Authentication needed");
39575
+ return fg8(PALETTE.approvalQuestion)("\u26A0 Authentication needed");
38568
39576
  if (status === "active")
38569
- return fg7(PALETTE.progress)(`${splashSpinnerFrames[frame % splashSpinnerFrames.length]} ${label === "Load sessions" ? "Loading sessions..." : label}`);
38570
- return fg7(PALETTE.dimText)(`\u25CB ${label}`);
39577
+ return fg8(PALETTE.progress)(`${splashSpinnerFrames[frame % splashSpinnerFrames.length]} ${label === "Load sessions" ? "Loading sessions..." : label}`);
39578
+ return fg8(PALETTE.dimText)(`\u25CB ${label}`);
38571
39579
  }
38572
39580
  function renderAuthenticationBand({ state, frame }) {
38573
39581
  if (!state.waitingForAuthentication)
38574
- return new StyledText6([]);
39582
+ return new StyledText7([]);
38575
39583
  const spinner = splashSpinnerFrames[frame % splashSpinnerFrames.length];
38576
- const url3 = state.verificationUrl ? [fg7(PALETTE.dimText)(`
38577
- Verification URL: `), bold5(fg7(PALETTE.humanAccent)(state.verificationUrl))] : [];
38578
- return new StyledText6([
38579
- fg7(PALETTE.bodyText)("Continue authentication in your browser."),
39584
+ const url3 = state.verificationUrl ? [fg8(PALETTE.dimText)(`
39585
+ Verification URL: `), bold5(fg8(PALETTE.humanAccent)(state.verificationUrl))] : [];
39586
+ return new StyledText7([
39587
+ fg8(PALETTE.bodyText)("Continue authentication in your browser."),
38580
39588
  ...url3,
38581
- fg7(PALETTE.dimText)(`
39589
+ fg8(PALETTE.dimText)(`
38582
39590
  ${spinner} Waiting for approval \xB7 q / esc cancel`)
38583
39591
  ]);
38584
39592
  }
@@ -38653,7 +39661,7 @@ function renderMark(field, frame, rows = markRows) {
38653
39661
  const flush = () => {
38654
39662
  if (runLength === 0)
38655
39663
  return;
38656
- chunks.push(fg7(grey(runTop))(bg5(grey(runBottom))(upperHalfBlock.repeat(runLength))));
39664
+ chunks.push(fg8(grey(runTop))(bg5(grey(runBottom))(upperHalfBlock.repeat(runLength))));
38657
39665
  runLength = 0;
38658
39666
  };
38659
39667
  for (let column = 0;column < columns; column++) {
@@ -38670,10 +39678,10 @@ function renderMark(field, frame, rows = markRows) {
38670
39678
  }
38671
39679
  flush();
38672
39680
  if (row < rows - 1)
38673
- chunks.push(fg7("#000000")(`
39681
+ chunks.push(fg8("#000000")(`
38674
39682
  `));
38675
39683
  }
38676
- return new StyledText6(chunks);
39684
+ return new StyledText7(chunks);
38677
39685
  }
38678
39686
  function shimmerBrightness(field, column, sampleRow, columns, sampleRows, center) {
38679
39687
  const x0 = Math.floor(column / columns * field.width);
@@ -38693,8 +39701,8 @@ function grey(value) {
38693
39701
  return `#${hex5}${hex5}${hex5}`;
38694
39702
  }
38695
39703
  function renderSplashTitle() {
38696
- return new StyledText6([
38697
- bold5(fg7(PALETTE.bodyText)("Remy CLI"))
39704
+ return new StyledText7([
39705
+ bold5(fg8(PALETTE.bodyText)("Remy CLI"))
38698
39706
  ]);
38699
39707
  }
38700
39708
 
@@ -39029,6 +40037,9 @@ function createRepositoryLister({ client }) {
39029
40037
  function createRepositorySuggester({ client }) {
39030
40038
  return async (input) => await suggestRemoteRepositories({ client, input });
39031
40039
  }
40040
+ function createBranchSuggester({ client }) {
40041
+ return async (input) => await suggestRemoteBranches({ client, input });
40042
+ }
39032
40043
  function parseOptionalStringFlag(value, name) {
39033
40044
  if (value === undefined)
39034
40045
  return;
@@ -39172,48 +40183,98 @@ async function dashboard({
39172
40183
  const openDashboardTui = dependencies.openDashboardTui ?? createDashboardTui;
39173
40184
  const pages = [initialSessions];
39174
40185
  let pageIndex = 0;
39175
- let loadingPage;
40186
+ let filters = { statuses: [], creators: [] };
40187
+ let authors;
40188
+ let activeDashboardGeneration = 0;
39176
40189
  function holdTransitionScreen() {
39177
40190
  showRemyTransitionScreen({ write: (chunk) => dependencies.output.writeStdout(chunk) });
39178
40191
  }
39179
- async function loadDashboardPage({ target, signal }) {
39180
- if (loadingPage)
39181
- return await loadingPage;
39182
- loadingPage = (async () => {
39183
- if (target === "current") {
39184
- const after = pageIndex > 0 ? pages[pageIndex - 1]?.nextCursor ?? undefined : undefined;
39185
- const refreshed = toDashboardPage(await operations.listSessions({
40192
+ function createDashboardPageLoader({ generation }) {
40193
+ const loadingPages = new Map;
40194
+ let pageLoadTail = Promise.resolve();
40195
+ return async (input) => {
40196
+ const requestKey = "filters" in input ? `filters:${JSON.stringify(input.filters)}` : input.target;
40197
+ const matchingPageLoad = loadingPages.get(requestKey);
40198
+ if (matchingPageLoad)
40199
+ return await matchingPageLoad;
40200
+ const pendingPageLoad = pageLoadTail;
40201
+ const pageLoad = pendingPageLoad.then(async () => {
40202
+ if (generation !== activeDashboardGeneration)
40203
+ return { ...pages[pageIndex], canGoPrevious: pageIndex > 0 };
40204
+ if ("filters" in input) {
40205
+ const nextFilters = input.filters;
40206
+ const first = toDashboardPage(await operations.listSessions({
40207
+ client: operations.client,
40208
+ limit: 20,
40209
+ ...nextFilters.statuses.length > 0 ? { statuses: nextFilters.statuses } : {},
40210
+ ...nextFilters.creators.length > 0 ? { creatorUserIds: nextFilters.creators.map((creator) => creator.id) } : {},
40211
+ signal: input.signal
40212
+ }));
40213
+ if (generation !== activeDashboardGeneration)
40214
+ return first;
40215
+ filters = nextFilters;
40216
+ pages.splice(0, pages.length, first);
40217
+ pageIndex = 0;
40218
+ return first;
40219
+ }
40220
+ const { target, signal } = input;
40221
+ if (target === "current") {
40222
+ const after = pageIndex > 0 ? pages[pageIndex - 1]?.nextCursor ?? undefined : undefined;
40223
+ const refreshed = toDashboardPage(await operations.listSessions({
40224
+ client: operations.client,
40225
+ limit: 20,
40226
+ after,
40227
+ ...filters.statuses.length > 0 ? { statuses: filters.statuses } : {},
40228
+ ...filters.creators.length > 0 ? { creatorUserIds: filters.creators.map((creator) => creator.id) } : {},
40229
+ signal
40230
+ }));
40231
+ if (generation !== activeDashboardGeneration)
40232
+ return refreshed;
40233
+ pages.splice(pageIndex, pages.length - pageIndex, refreshed);
40234
+ return { ...refreshed, canGoPrevious: pageIndex > 0 };
40235
+ }
40236
+ if (target === "previous") {
40237
+ pageIndex = Math.max(0, pageIndex - 1);
40238
+ return { ...pages[pageIndex], canGoPrevious: pageIndex > 0 };
40239
+ }
40240
+ const current = pages[pageIndex];
40241
+ if (!current.hasMore || !current.nextCursor)
40242
+ return { ...current, canGoPrevious: pageIndex > 0 };
40243
+ const next = toDashboardPage(await operations.listSessions({
39186
40244
  client: operations.client,
39187
40245
  limit: 20,
39188
- after,
40246
+ after: current.nextCursor,
40247
+ ...filters.statuses.length > 0 ? { statuses: filters.statuses } : {},
40248
+ ...filters.creators.length > 0 ? { creatorUserIds: filters.creators.map((creator) => creator.id) } : {},
39189
40249
  signal
39190
40250
  }));
39191
- pages.splice(pageIndex, pages.length - pageIndex, refreshed);
39192
- return { ...refreshed, canGoPrevious: pageIndex > 0 };
39193
- }
39194
- if (target === "previous") {
39195
- pageIndex = Math.max(0, pageIndex - 1);
39196
- return { ...pages[pageIndex], canGoPrevious: pageIndex > 0 };
40251
+ if (generation !== activeDashboardGeneration)
40252
+ return next;
40253
+ pages.splice(pageIndex + 1);
40254
+ pages.push(next);
40255
+ pageIndex += 1;
40256
+ return { ...next, canGoPrevious: pageIndex > 0 };
40257
+ });
40258
+ loadingPages.set(requestKey, pageLoad);
40259
+ pageLoadTail = pageLoad.then(() => {
40260
+ return;
40261
+ }, () => {
40262
+ return;
40263
+ });
40264
+ try {
40265
+ return await pageLoad;
40266
+ } finally {
40267
+ if (loadingPages.get(requestKey) === pageLoad)
40268
+ loadingPages.delete(requestKey);
39197
40269
  }
39198
- const current = pages[pageIndex];
39199
- if (!current.hasMore || !current.nextCursor)
39200
- return { ...current, canGoPrevious: pageIndex > 0 };
39201
- const next = toDashboardPage(await operations.listSessions({
39202
- client: operations.client,
39203
- limit: 20,
39204
- after: current.nextCursor,
39205
- signal
39206
- }));
39207
- pages.splice(pageIndex + 1);
39208
- pages.push(next);
39209
- pageIndex += 1;
39210
- return { ...next, canGoPrevious: pageIndex > 0 };
39211
- })();
39212
- try {
39213
- return await loadingPage;
39214
- } finally {
39215
- loadingPage = undefined;
39216
- }
40270
+ };
40271
+ }
40272
+ async function loadDashboardAuthors() {
40273
+ if (authors)
40274
+ return authors;
40275
+ const loadedAuthors = await collectAllUsers({ listUsers: operations.listUsers, signal: dependencies.abortSignal });
40276
+ authors = loadedAuthors;
40277
+ return loadedAuthors;
39217
40278
  }
39218
40279
  let firstOpen = true;
39219
40280
  holdTransitionScreen();
@@ -39222,17 +40283,30 @@ async function dashboard({
39222
40283
  pages.splice(0, pages.length, toDashboardPage(await operations.listSessions({
39223
40284
  client: operations.client,
39224
40285
  limit: 20,
40286
+ ...filters.statuses.length > 0 ? { statuses: filters.statuses } : {},
40287
+ ...filters.creators.length > 0 ? { creatorUserIds: filters.creators.map((creator) => creator.id) } : {},
39225
40288
  signal: dependencies.abortSignal
39226
40289
  })));
39227
40290
  pageIndex = 0;
39228
40291
  }
39229
40292
  firstOpen = false;
40293
+ const dashboardGeneration = ++activeDashboardGeneration;
40294
+ const loadDashboardPage = createDashboardPageLoader({ generation: dashboardGeneration });
39230
40295
  const action = await renderRemyView({
39231
40296
  open: async () => await openDashboardTui({
39232
40297
  initialPage: pages[0],
40298
+ initialFilters: filters,
40299
+ loadAuthors: loadDashboardAuthors,
39233
40300
  loadPage: loadDashboardPage
39234
40301
  }),
39235
- waitForAction: async (dashboardTui) => await awaitWithAbort({ value: dashboardTui.waitForAction(), abortSignal: dependencies.abortSignal })
40302
+ waitForAction: async (dashboardTui) => {
40303
+ try {
40304
+ return await awaitWithAbort({ value: dashboardTui.waitForAction(), abortSignal: dependencies.abortSignal });
40305
+ } finally {
40306
+ if (activeDashboardGeneration === dashboardGeneration)
40307
+ activeDashboardGeneration += 1;
40308
+ }
40309
+ }
39236
40310
  });
39237
40311
  if (!action)
39238
40312
  return 0;
@@ -39275,12 +40349,12 @@ async function prepareDashboardBootstrap({
39275
40349
  return;
39276
40350
  });
39277
40351
  onLoadingSessions?.();
39278
- const initialSessions = toDashboardPage(await resolvedOperations.listSessions({
40352
+ const initialSessionPage = await resolvedOperations.listSessions({
39279
40353
  client: resolvedOperations.client,
39280
40354
  limit: 20,
39281
40355
  signal: dependencies.abortSignal
39282
- }));
39283
- return { operations: resolvedOperations, repositories, initialSessions };
40356
+ });
40357
+ return { operations: resolvedOperations, repositories, initialSessions: toDashboardPage(initialSessionPage) };
39284
40358
  }
39285
40359
  async function prepareDashboardWithStartupSplash({
39286
40360
  dependencies,
@@ -39471,11 +40545,15 @@ async function createNewSession({
39471
40545
  throw new Error("All selected repositories must belong to the same GitHub installation.");
39472
40546
  const fileIds = [];
39473
40547
  for (const attachment of command.attachments) {
39474
- fileIds.push(await operations.reserveAndUploadFile({
39475
- client: operations.client,
40548
+ const uploaded = await uploadLocalAttachment({
39476
40549
  filePath: attachment,
39477
- idempotencyKey: randomUUID5()
39478
- }));
40550
+ reserveAndUploadFile: async (input) => await operations.reserveAndUploadFile({
40551
+ ...input,
40552
+ client: operations.client,
40553
+ idempotencyKey: randomUUID5()
40554
+ })
40555
+ });
40556
+ fileIds.push(uploaded.fileId);
39479
40557
  }
39480
40558
  const created = await operations.createCodexSession({
39481
40559
  client: operations.client,
@@ -39549,6 +40627,7 @@ async function createFromTuiDraft({
39549
40627
  input: {
39550
40628
  installationId: draft.githubInstallationId,
39551
40629
  repositoryIds: draft.repositories.map((repository) => repository.id),
40630
+ repositoryBranchOverrides: draft.repositoryBranchOverrides ?? [],
39552
40631
  prompt: draft.prompt,
39553
40632
  fileIds: draft.attachments.map((attachment) => attachment.fileId),
39554
40633
  model: draft.model,
@@ -39571,10 +40650,11 @@ async function openTuiNewSessionWizard({
39571
40650
  repositories,
39572
40651
  reloadRepositories,
39573
40652
  suggestRepositories: operations.suggestRepositories,
40653
+ suggestBranches: operations.suggestBranches,
39574
40654
  initialDraft,
39575
40655
  initialError,
39576
40656
  readClipboardImage,
39577
- savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir() }),
40657
+ savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir2() }),
39578
40658
  attachPromptPaths: async (input) => await uploadPromptPaths({ operations, ...input })
39579
40659
  }),
39580
40660
  waitForAction: async (wizard) => await awaitWithAbort({ value: wizard.waitForDraft(), abortSignal: dependencies.abortSignal })
@@ -39583,15 +40663,20 @@ async function openTuiNewSessionWizard({
39583
40663
  async function uploadPromptPaths({
39584
40664
  operations,
39585
40665
  text,
39586
- excludedPaths
40666
+ excludedPaths,
40667
+ selectedPaths,
40668
+ onUploadStart
39587
40669
  }) {
39588
40670
  return await attachPromptPaths({
39589
40671
  text,
39590
40672
  excludedPaths,
40673
+ selectedPaths,
40674
+ onUploadStart,
39591
40675
  cwd: process.cwd(),
39592
- reserveAndUploadFile: async ({ filePath }) => await operations.reserveAndUploadFile({
40676
+ reserveAndUploadFile: async ({ filePath, mediaType }) => await operations.reserveAndUploadFile({
39593
40677
  client: operations.client,
39594
40678
  filePath,
40679
+ ...mediaType ? { mediaType } : {},
39595
40680
  idempotencyKey: randomUUID5()
39596
40681
  })
39597
40682
  });
@@ -39649,7 +40734,7 @@ async function runAttachedSession({
39649
40734
  ...activeMessageId ? { activeMessageId } : {},
39650
40735
  environment: dependencies.environment,
39651
40736
  getSession: async ({ sessionId: id }) => await operations.getSession({ client: operations.client, sessionId: id }),
39652
- listSessionEvents: async ({ sessionId: id, limit, after }) => await operations.listSessionEvents({ client: operations.client, sessionId: id, limit, after }),
40737
+ listSessionEvents: async ({ sessionId: id, limit, after, signal }) => await operations.listSessionEvents({ client: operations.client, sessionId: id, limit, after, signal }),
39653
40738
  openEventStream: ({ sessionId: id, lastRetainedEventId, signal, onSynchronized }) => operations.streamSessionEvents({ client: operations.client, sessionId: id, lastRetainedEventId, signal, onSynchronized })
39654
40739
  });
39655
40740
  const interactive = !noTui && !json3 && isInteractiveTerminal(dependencies);
@@ -39688,8 +40773,14 @@ async function runAttachedSession({
39688
40773
  });
39689
40774
  }
39690
40775
  const startPromise = controller.start(start);
39691
- await Promise.race([controller.waitUntilReady(), startPromise]);
40776
+ await Promise.race([
40777
+ controller.waitUntilReady(),
40778
+ startPromise,
40779
+ ...aborted3 ? [aborted3] : []
40780
+ ]);
39692
40781
  if (interactive) {
40782
+ if (dependencies.abortSignal?.aborted)
40783
+ throw dependencies.abortSignal.reason ?? new Error("interrupted");
39693
40784
  const openSessionTui = dependencies.openSessionTui ?? createSessionTui;
39694
40785
  let composerSnapshot;
39695
40786
  const holdTransitionScreen = () => showRemyTransitionScreen({ write: (chunk) => dependencies.output.writeStdout(chunk) });
@@ -39726,7 +40817,7 @@ async function runAttachedSession({
39726
40817
  controller.updateDetail(detail);
39727
40818
  },
39728
40819
  readClipboardImage,
39729
- savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir() }),
40820
+ savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir2() }),
39730
40821
  attachPromptPaths: async (input) => await uploadPromptPaths({ operations, ...input })
39731
40822
  }),
39732
40823
  waitForAction: async (tui) => await Promise.race([tui.waitForAction(), aborted3])
@@ -39835,7 +40926,9 @@ async function createSessionOperations(dependencies, { onAuthenticated } = {}) {
39835
40926
  return {
39836
40927
  client: dependencies.sessionClient,
39837
40928
  listRepositories: dependencies.listRepositories ?? (async () => await Promise.reject(new Error("Missing remote repository operation."))),
40929
+ listUsers: dependencies.listUsers ?? (async (input) => await listRemoteUsers({ client: dependencies.sessionClient, ...input })),
39838
40930
  suggestRepositories: dependencies.suggestRepositories ?? (async () => await Promise.reject(new Error("Missing remote repository suggestions operation."))),
40931
+ suggestBranches: dependencies.suggestBranches ?? (async () => await Promise.reject(new Error("Missing remote branch suggestions operation."))),
39839
40932
  reserveAndUploadFile: dependencies.reserveAndUploadFile ?? reserveAndUploadFile,
39840
40933
  createCodexSession: dependencies.createCodexSession ?? createCodexSession,
39841
40934
  appendSessionMessage: dependencies.appendSessionMessage ?? appendSessionMessage,
@@ -39853,7 +40946,9 @@ async function createSessionOperations(dependencies, { onAuthenticated } = {}) {
39853
40946
  return {
39854
40947
  client,
39855
40948
  listRepositories: dependencies.listRepositories ?? createRepositoryLister({ client }),
40949
+ listUsers: dependencies.listUsers ?? (async (input) => await listRemoteUsers({ client, ...input })),
39856
40950
  suggestRepositories: dependencies.suggestRepositories ?? createRepositorySuggester({ client }),
40951
+ suggestBranches: dependencies.suggestBranches ?? createBranchSuggester({ client }),
39857
40952
  reserveAndUploadFile: dependencies.reserveAndUploadFile ?? reserveAndUploadFile,
39858
40953
  createCodexSession: dependencies.createCodexSession ?? createCodexSession,
39859
40954
  appendSessionMessage: dependencies.appendSessionMessage ?? appendSessionMessage,
@@ -39895,6 +40990,20 @@ async function collectAllRepositories({
39895
40990
  after = page.nextCursor;
39896
40991
  }
39897
40992
  }
40993
+ async function collectAllUsers({
40994
+ listUsers,
40995
+ signal
40996
+ }) {
40997
+ const users = [];
40998
+ let after;
40999
+ while (true) {
41000
+ const page = await listUsers({ limit: 100, after, signal });
41001
+ users.push(...page.data);
41002
+ if (!page.hasMore || !page.nextCursor)
41003
+ return users;
41004
+ after = page.nextCursor;
41005
+ }
41006
+ }
39898
41007
  function resolveSessionCachePathForCommand({ dependencies, sessionId }) {
39899
41008
  const stateBase = dependencies.environment.XDG_STATE_HOME ?? (dependencies.environment.HOME ? join6(dependencies.environment.HOME, ".local/state") : undefined);
39900
41009
  if (!stateBase)
@@ -40021,7 +41130,7 @@ Options:
40021
41130
  --installation <id> Select a GitHub installation
40022
41131
  --model <name> Select an agent model
40023
41132
  --reasoning-effort <low|medium|high|xhigh> Select agent reasoning effort
40024
- --attach <path> Upload a file; repeat for more attachments
41133
+ --attach <path> Upload a file or directory (20 MiB max); repeat as needed
40025
41134
  --no-tui Do not open the interactive terminal view
40026
41135
  --json Write session updates as JSON
40027
41136
  --help, -h Show this help
@@ -40170,7 +41279,7 @@ async function removeCommittedTokenRecoveryArtifacts({
40170
41279
  const directory = dirname6(tokenPath);
40171
41280
  let entries;
40172
41281
  try {
40173
- entries = await readdir2(directory);
41282
+ entries = await readdir3(directory);
40174
41283
  } catch (error93) {
40175
41284
  if (isMissingFileError3(error93))
40176
41285
  return;