@meistrari/remy-cli 1.13.0 → 1.14.1

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 +1532 -398
  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 });
@@ -33908,6 +34069,10 @@ var sessionMessageTurnAssociatedEventSchema = exports_external2.object({
33908
34069
  type: exports_external2.literal("session.message.turn-associated"),
33909
34070
  payload: exports_external2.object({ messageId: exports_external2.string().min(1), commandId: exports_external2.string().min(1), turnId: exports_external2.string().min(1) }).passthrough()
33910
34071
  }).passthrough();
34072
+ var sessionMessageDispatchEventSchema = exports_external2.object({
34073
+ type: exports_external2.enum(["session.message.dispatched", "session.message.withdrawn", "session.message.failed"]),
34074
+ payload: exports_external2.object({ messageId: exports_external2.string().min(1), commandId: exports_external2.string().min(1) }).passthrough()
34075
+ }).passthrough();
33911
34076
  var sessionWorkspaceGitInitializedEventSchema = exports_external2.object({
33912
34077
  type: exports_external2.literal("session.workspace.git.initialized"),
33913
34078
  payload: exports_external2.object({
@@ -33971,7 +34136,9 @@ function createSessionViewState({ detail, activeMessageId }) {
33971
34136
  kind: "session-view",
33972
34137
  sessionId: detail.id,
33973
34138
  ...detail.sessionNumber === undefined ? {} : { sessionNumber: detail.sessionNumber },
34139
+ title: detail.title ?? null,
33974
34140
  aggregateStatus: detail.status,
34141
+ ...detail.agentStatus === undefined ? {} : { agentStatus: detail.agentStatus },
33975
34142
  connectionStatus: "connected",
33976
34143
  pullRequest: toSessionPullRequest(detail),
33977
34144
  ...activeMessageId !== undefined ? { activeMessageId } : {},
@@ -34004,7 +34171,9 @@ function updateSessionDetail({ state, detail }) {
34004
34171
  ...state,
34005
34172
  sessionId: detail.id,
34006
34173
  ...detail.sessionNumber === undefined ? {} : { sessionNumber: detail.sessionNumber },
34174
+ ..."title" in detail ? { title: detail.title ?? null } : {},
34007
34175
  aggregateStatus: detail.status,
34176
+ ...detail.agentStatus === undefined ? {} : { agentStatus: detail.agentStatus },
34008
34177
  pullRequest: toSessionPullRequest(detail),
34009
34178
  connectionPreviews: toConnectionPreviews(detail)
34010
34179
  };
@@ -34040,6 +34209,8 @@ function sessionTurnOutcome({ state, sessionMessageId }) {
34040
34209
  const turn = state.messageTurns[sessionMessageId];
34041
34210
  if (!turn)
34042
34211
  return { status: "unassociated" };
34212
+ if (turn.turnId === null)
34213
+ return { status: "ended", turnId: null, outcome: turn.outcome };
34043
34214
  if (turn.outcome)
34044
34215
  return { status: "ended", turnId: turn.turnId, outcome: turn.outcome };
34045
34216
  return { status: "pending", turnId: turn.turnId };
@@ -34063,10 +34234,11 @@ function projectRetainedEvent({
34063
34234
  }) {
34064
34235
  const messageCreated = sessionMessageCreatedEventSchema.safeParse(event);
34065
34236
  const turnAssociated = sessionMessageTurnAssociatedEventSchema.safeParse(event);
34237
+ const messageDispatch = sessionMessageDispatchEventSchema.safeParse(event);
34066
34238
  const workspaceGitInitialized = sessionWorkspaceGitInitializedEventSchema.safeParse(event);
34067
34239
  const workspaceGitRevision = sessionWorkspaceGitRevisionEventSchema.safeParse(event);
34068
34240
  const turnEnded = agentTurnEndedEventSchema.safeParse(event);
34069
- let projected = messageCreated.success ? projectSessionMessageCreated({ state, event: messageCreated.data, occurredAt }) : turnAssociated.success ? projectSessionMessageTurnAssociated({ state, event: turnAssociated.data }) : workspaceGitInitialized.success ? projectSessionWorkspaceGitInitialized({ state, event: workspaceGitInitialized.data, occurredAt, retainedEventId }) : workspaceGitRevision.success ? projectSessionWorkspaceGitRevision({ state, event: workspaceGitRevision.data, occurredAt, retainedEventId }) : turnEnded.success ? projectAgentTurnEnded({ state, event: turnEnded.data, occurredAt, retainedEventId }) : projectDurableAgentEvent({ state, event, occurredAt, retainedEventId });
34241
+ let projected = messageCreated.success ? projectSessionMessageCreated({ state, event: messageCreated.data, occurredAt }) : turnAssociated.success ? projectSessionMessageTurnAssociated({ state, event: turnAssociated.data }) : messageDispatch.success ? projectSessionMessageDispatch({ state, event: messageDispatch.data, occurredAt, retainedEventId }) : workspaceGitInitialized.success ? projectSessionWorkspaceGitInitialized({ state, event: workspaceGitInitialized.data, occurredAt, retainedEventId }) : workspaceGitRevision.success ? projectSessionWorkspaceGitRevision({ state, event: workspaceGitRevision.data, occurredAt, retainedEventId }) : turnEnded.success ? projectAgentTurnEnded({ state, event: turnEnded.data, occurredAt, retainedEventId }) : projectDurableAgentEvent({ state, event, occurredAt, retainedEventId });
34070
34242
  projected = recordAgentLineageEvent({ state: projected, event, retainedEventId });
34071
34243
  if (artifact) {
34072
34244
  const publication = artifact.kind === "file" ? { artifactId: artifact.artifact_id, kind: artifact.kind, title: artifact.filename } : { artifactId: artifact.artifact_id, kind: artifact.kind, title: artifact.title, url: artifact.url };
@@ -34076,7 +34248,9 @@ function projectRetainedEvent({
34076
34248
  artifactId: artifact.artifact_id,
34077
34249
  occurredAt,
34078
34250
  title: artifact.filename,
34079
- mediaType: artifact.media_type
34251
+ mediaType: artifact.media_type,
34252
+ ...artifact.download_url === undefined ? {} : { downloadUrl: artifact.download_url },
34253
+ ...artifact.preview_url === undefined ? {} : { previewUrl: artifact.preview_url }
34080
34254
  } : {
34081
34255
  kind: "artifact",
34082
34256
  artifactKind: artifact.kind,
@@ -34152,6 +34326,24 @@ function projectSessionMessageTurnAssociated({ state, event }) {
34152
34326
  };
34153
34327
  return { ...state, messageTurns: { ...state.messageTurns, [event.payload.messageId]: nextTurn } };
34154
34328
  }
34329
+ function projectSessionMessageDispatch({ state, event, occurredAt, retainedEventId }) {
34330
+ if (event.type === "session.message.dispatched")
34331
+ return state;
34332
+ const withdrawn = event.type === "session.message.withdrawn";
34333
+ const outcome = withdrawn ? "cancelled" : "failed";
34334
+ return appendActivity({
34335
+ state: {
34336
+ ...state,
34337
+ messageTurns: {
34338
+ ...state.messageTurns,
34339
+ [event.payload.messageId]: { commandId: event.payload.commandId, turnId: null, outcome }
34340
+ }
34341
+ },
34342
+ occurredAt,
34343
+ retainedEventId,
34344
+ card: withdrawn ? { kind: "lifecycle", weight: "signal", title: "Message withdrawn", summary: "Message was withdrawn before reaching Remy." } : { kind: "failure", weight: "signal", title: "Message failed", summary: "Message failed before reaching Remy." }
34345
+ });
34346
+ }
34155
34347
  function projectSessionWorkspaceGitInitialized({ state, event, occurredAt, retainedEventId }) {
34156
34348
  return appendActivity({
34157
34349
  state,
@@ -35061,12 +35253,12 @@ function createRemoteSessionController(dependencies) {
35061
35253
  detail: input.detail,
35062
35254
  activeMessageId: dependencies.activeMessageId ?? cache?.activeMessageId
35063
35255
  });
35064
- resolveReady();
35065
- publishState();
35066
35256
  if (input.mode === "cold-resume") {
35067
35257
  const hydrationGeneration = beginReasoningAttempt();
35068
35258
  await hydrateRetainedHistoryFromBeginning(hydrationGeneration);
35069
35259
  }
35260
+ resolveReady();
35261
+ publishState();
35070
35262
  await streamWithControllerReconnects();
35071
35263
  }
35072
35264
  function stop() {
@@ -35102,7 +35294,8 @@ function createRemoteSessionController(dependencies) {
35102
35294
  async function recordActiveMessage(sessionMessageId) {
35103
35295
  state = {
35104
35296
  ...getState(),
35105
- activeMessageId: sessionMessageId
35297
+ activeMessageId: sessionMessageId,
35298
+ agentStatus: undefined
35106
35299
  };
35107
35300
  await writeCache();
35108
35301
  publishState();
@@ -35123,7 +35316,8 @@ function createRemoteSessionController(dependencies) {
35123
35316
  const page = await dependencies.listSessionEvents({
35124
35317
  sessionId: dependencies.sessionId,
35125
35318
  limit: sessionHistoryPageSize,
35126
- after
35319
+ after,
35320
+ signal: abortController.signal
35127
35321
  });
35128
35322
  for (const item of page.data) {
35129
35323
  await reduceFrameAndPersist({
@@ -35377,7 +35571,10 @@ async function sleepMs(ms, signal) {
35377
35571
  }
35378
35572
 
35379
35573
  // 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";
35574
+ import { bg, BoxRenderable as BoxRenderable2, bold as bold2, CliRenderEvents as CliRenderEvents2, fg as fg3, ScrollBoxRenderable, stringToStyledText as stringToStyledText2, TextRenderable as TextRenderable2 } from "@opentui/core";
35575
+
35576
+ // src/tui/composer.ts
35577
+ import { BoxRenderable, TextRenderable, TextareaRenderable } from "@opentui/core";
35381
35578
 
35382
35579
  // src/tui/composer-divider.ts
35383
35580
  import { dim as dim2, fg as fg2, StyledText as StyledText2 } from "@opentui/core";
@@ -35572,6 +35769,98 @@ function renderComposerDivider({ width, tag, leadLabel }) {
35572
35769
  ]);
35573
35770
  }
35574
35771
 
35772
+ // src/tui/composer-height.ts
35773
+ var composerMaxRows = 6;
35774
+ function resizeComposer(composer) {
35775
+ composer.height = Math.min(Math.max(composer.editorView.getTotalVirtualLineCount(), 1), composerMaxRows);
35776
+ }
35777
+
35778
+ // src/tui/composer.ts
35779
+ var defaultPlaceholder = "Message Remy\u2026 Tab paths attach \xB7 Enter/Shift+Enter send \xB7 Option+Enter newline";
35780
+ function createComposer({
35781
+ renderer,
35782
+ parent,
35783
+ tag,
35784
+ topDividerLabel,
35785
+ placeholder = defaultPlaceholder,
35786
+ screenPaddingX = 1,
35787
+ onSubmit,
35788
+ onContentChange
35789
+ }) {
35790
+ const root = new BoxRenderable(renderer, { width: "100%", flexDirection: "column", flexShrink: 0 });
35791
+ const dividerTop = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35792
+ const textarea = new TextareaRenderable(renderer, {
35793
+ width: "100%",
35794
+ height: 1,
35795
+ flexShrink: 0,
35796
+ wrapMode: "word",
35797
+ placeholder,
35798
+ keyBindings: [
35799
+ { name: "return", action: "submit" },
35800
+ { name: "return", shift: true, action: "submit" },
35801
+ { name: "return", meta: true, action: "newline" },
35802
+ { name: "j", ctrl: true, action: "newline" }
35803
+ ],
35804
+ onSubmit
35805
+ });
35806
+ const dividerBottom = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35807
+ let mounted = false;
35808
+ root.add(dividerTop);
35809
+ root.add(textarea);
35810
+ root.add(dividerBottom);
35811
+ textarea.onContentChange = () => {
35812
+ resizeComposer(textarea);
35813
+ onContentChange?.();
35814
+ };
35815
+ function render() {
35816
+ const dividerWidth = renderer.width - screenPaddingX * 2;
35817
+ dividerTop.content = renderComposerDivider({ width: dividerWidth, tag, leadLabel: topDividerLabel?.() });
35818
+ dividerBottom.content = renderComposerDivider({ width: dividerWidth });
35819
+ }
35820
+ return {
35821
+ textarea,
35822
+ mount() {
35823
+ if (mounted)
35824
+ return;
35825
+ mounted = true;
35826
+ parent.add(root);
35827
+ resizeComposer(textarea);
35828
+ render();
35829
+ },
35830
+ unmount() {
35831
+ if (!mounted)
35832
+ return;
35833
+ mounted = false;
35834
+ parent.remove(root);
35835
+ },
35836
+ render,
35837
+ focus() {
35838
+ textarea.focus();
35839
+ },
35840
+ destroy() {
35841
+ if (mounted)
35842
+ parent.remove(root);
35843
+ mounted = false;
35844
+ root.destroyRecursively();
35845
+ }
35846
+ };
35847
+ }
35848
+
35849
+ // src/tui/fuzzy-match.ts
35850
+ function fuzzyMatches(value, query) {
35851
+ const normalizedQuery = query.replaceAll(/\s/g, "").toLowerCase();
35852
+ if (!normalizedQuery)
35853
+ return true;
35854
+ let queryIndex = 0;
35855
+ for (const character of value.toLowerCase()) {
35856
+ if (character === normalizedQuery[queryIndex])
35857
+ queryIndex += 1;
35858
+ if (queryIndex === normalizedQuery.length)
35859
+ return true;
35860
+ }
35861
+ return false;
35862
+ }
35863
+
35575
35864
  // src/tui/renderer.ts
35576
35865
  import { CliRenderEvents, createClipboard, createCliRenderer, createHostClipboard, createRendererClipboardAdapter } from "@opentui/core";
35577
35866
  var rendererDestroyed = new WeakMap;
@@ -35654,8 +35943,17 @@ async function renderRemyView({
35654
35943
 
35655
35944
  // src/tui/dashboard.ts
35656
35945
  var DASHBOARD_POLL_INTERVAL_MS = 1e4;
35946
+ var lifecycleStatusOptions = [
35947
+ { value: "open", label: "Open" },
35948
+ { value: "completed", label: "Completed" },
35949
+ { value: "failed", label: "Failed" },
35950
+ { value: "cancelled", label: "Cancelled" }
35951
+ ];
35657
35952
  async function createDashboardTui({
35658
35953
  initialPage,
35954
+ initialFilters = { statuses: [], creators: [] },
35955
+ authors: initialAuthors = [],
35956
+ loadAuthors,
35659
35957
  loadPage,
35660
35958
  createRenderer = createDefaultRenderer
35661
35959
  }) {
@@ -35664,6 +35962,7 @@ async function createDashboardTui({
35664
35962
  let keyHandler;
35665
35963
  let resizeHandler;
35666
35964
  let refreshTimer;
35965
+ let contentScrollHandler;
35667
35966
  let rendererDestroyed2 = false;
35668
35967
  const loadPageAbortController = new AbortController;
35669
35968
  try {
@@ -35675,28 +35974,61 @@ async function createDashboardTui({
35675
35974
  loadPageAbortController.abort();
35676
35975
  }, render = function() {
35677
35976
  const contentWidth = renderer.width - 4;
35678
- const rowsBelow = rowsBelowVisibleWindow({ sessionCount: page.sessions.length, selectedIndex, height: renderer.height });
35977
+ const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
35978
+ const rowsBelow = rowsBelowVisibleWindow({ sessionCount: sessions.length, selectedIndex, height: renderer.height });
35679
35979
  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({
35980
+ const selectedSession = filterMode ? undefined : sessions[selectedIndex];
35981
+ 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
35982
  session: selectedSession,
35683
35983
  selectedIndex,
35684
- sessionCount: page.sessions.length,
35984
+ sessionCount: sessions.length,
35685
35985
  width: contentWidth
35686
35986
  }) : formatSessionList({
35687
- sessions: page.sessions,
35987
+ sessions,
35688
35988
  selectedIndex,
35689
35989
  width: contentWidth,
35690
35990
  height: renderer.height
35691
35991
  });
35692
- statusBand.content = dashboardStatusBand({ page, pageIndex });
35992
+ statusBand.content = dashboardStatusBand({ page, pageIndex, filters, sessionSearchQuery });
35693
35993
  content.content = joinStyled([stringToStyledText2(""), sessionRows], `
35694
35994
  `);
35995
+ desiredContentScrollY = filterMode ? filterMode.selectedIndex + 2 : 0;
35996
+ if (!contentScrollHandler) {
35997
+ contentScrollHandler = () => {
35998
+ contentScrollHandler = undefined;
35999
+ if (destroyed)
36000
+ return;
36001
+ listRegion.scrollTo({ x: 0, y: desiredContentScrollY });
36002
+ renderer.requestRender();
36003
+ };
36004
+ renderer.once(CliRenderEvents2.FRAME, contentScrollHandler);
36005
+ }
35695
36006
  const showPreview = !isNarrow && selectedSession !== undefined;
35696
36007
  previewBand.visible = showPreview;
35697
36008
  previewBand.content = showPreview ? formatSessionPreview({ session: selectedSession, width: contentWidth }) : stringToStyledText2("");
35698
- separator.content = renderComposerDivider({ width: contentWidth });
35699
- footer.content = dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen });
36009
+ const activeSearchComposer = filterMode?.searching ? "filter" : sessionSearchOpen ? "session" : undefined;
36010
+ if (activeSearchComposer !== mountedSearchComposer) {
36011
+ if (mountedSearchComposer === "filter")
36012
+ filterSearchComposer.unmount();
36013
+ else if (mountedSearchComposer === "session")
36014
+ sessionSearchComposer.unmount();
36015
+ mountedSearchComposer = activeSearchComposer;
36016
+ if (activeSearchComposer === "filter") {
36017
+ filterSearchComposer.mount();
36018
+ filterSearchComposer.focus();
36019
+ } else if (activeSearchComposer === "session") {
36020
+ sessionSearchComposer.mount();
36021
+ sessionSearchComposer.focus();
36022
+ } else {
36023
+ listRegion.focus();
36024
+ }
36025
+ }
36026
+ if (activeSearchComposer === "filter")
36027
+ filterSearchComposer.render();
36028
+ else if (activeSearchComposer === "session")
36029
+ sessionSearchComposer.render();
36030
+ separator.content = activeSearchComposer ? "" : renderComposerDivider({ width: contentWidth });
36031
+ footer.content = filterMode ? dashboardFilterFooter({ mode: filterMode, loadingPage, loadingAuthors, pageError, authorError }) : dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen, sessionSearchOpen });
35700
36032
  renderer.requestRender();
35701
36033
  }, finish = function(value) {
35702
36034
  if (settled)
@@ -35711,9 +36043,40 @@ async function createDashboardTui({
35711
36043
  stopPageRefresh();
35712
36044
  rejectDraft(error93);
35713
36045
  }, moveSelectionTo = function(index) {
35714
- if (page.sessions.length === 0)
36046
+ const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
36047
+ if (sessions.length === 0)
36048
+ return;
36049
+ selectedIndex = Math.max(0, Math.min(index, sessions.length - 1));
36050
+ render();
36051
+ }, applyFilterSearch = function() {
36052
+ if (!filterMode?.searching)
36053
+ return;
36054
+ filterMode = { ...filterMode, searching: false, selectedIndex: 0 };
36055
+ filterSearchComposer.textarea.setText("");
36056
+ render();
36057
+ }, cancelFilterSearch = function() {
36058
+ if (!filterMode?.searching)
36059
+ return;
36060
+ filterMode = { ...filterMode, searching: false, query: "", selectedIndex: 0 };
36061
+ filterSearchComposer.textarea.setText("");
36062
+ render();
36063
+ }, openSessionSearch = function() {
36064
+ sessionSearchOpen = true;
36065
+ sessionSearchComposer.textarea.setText(sessionSearchQuery);
36066
+ render();
36067
+ }, applySessionSearch = function() {
36068
+ if (!sessionSearchOpen)
35715
36069
  return;
35716
- selectedIndex = Math.max(0, Math.min(index, page.sessions.length - 1));
36070
+ sessionSearchOpen = false;
36071
+ selectedIndex = 0;
36072
+ render();
36073
+ }, cancelSessionSearch = function() {
36074
+ if (!sessionSearchOpen)
36075
+ return;
36076
+ sessionSearchOpen = false;
36077
+ sessionSearchQuery = "";
36078
+ selectedIndex = 0;
36079
+ sessionSearchComposer.textarea.setText("");
35717
36080
  render();
35718
36081
  }, handleSessionKey = function(key) {
35719
36082
  if (logoutConfirmationOpen) {
@@ -35726,6 +36089,84 @@ async function createDashboardTui({
35726
36089
  }
35727
36090
  return;
35728
36091
  }
36092
+ if (sessionSearchOpen) {
36093
+ if (key.name === "escape") {
36094
+ key.preventDefault();
36095
+ cancelSessionSearch();
36096
+ }
36097
+ return;
36098
+ }
36099
+ if (filterMode) {
36100
+ const options = visibleDashboardFilterOptions({ mode: filterMode, authors });
36101
+ if (key.name === "escape") {
36102
+ key.preventDefault();
36103
+ if (filterMode.searching) {
36104
+ cancelFilterSearch();
36105
+ return;
36106
+ }
36107
+ filterMode = undefined;
36108
+ pageError = undefined;
36109
+ authorError = undefined;
36110
+ render();
36111
+ return;
36112
+ }
36113
+ if (filterMode.searching)
36114
+ return;
36115
+ if (loadingAuthors) {
36116
+ key.preventDefault();
36117
+ return;
36118
+ }
36119
+ const filterListFocused = renderer.currentFocusedRenderable === listRegion;
36120
+ if (filterListFocused && (key.name === "G" || key.name === "g" && key.shift)) {
36121
+ key.preventDefault();
36122
+ awaitingVimGoToTop = false;
36123
+ if (options.length > 0)
36124
+ filterMode = { ...filterMode, selectedIndex: options.length - 1 };
36125
+ render();
36126
+ return;
36127
+ }
36128
+ if (filterListFocused && key.name === "g") {
36129
+ key.preventDefault();
36130
+ if (awaitingVimGoToTop && options.length > 0)
36131
+ filterMode = { ...filterMode, selectedIndex: 0 };
36132
+ awaitingVimGoToTop = !awaitingVimGoToTop;
36133
+ render();
36134
+ return;
36135
+ }
36136
+ awaitingVimGoToTop = false;
36137
+ if (key.name === "down" || key.name === "up" || key.name === "j" || key.name === "k") {
36138
+ key.preventDefault();
36139
+ if (options.length > 0) {
36140
+ const backwards = key.name === "up" || key.name === "k";
36141
+ filterMode = { ...filterMode, selectedIndex: (filterMode.selectedIndex + (backwards ? -1 : 1) + options.length) % options.length };
36142
+ }
36143
+ render();
36144
+ return;
36145
+ }
36146
+ if (key.name === "space" || key.name === "left" || key.name === "right" || key.name === "h" || key.name === "l") {
36147
+ key.preventDefault();
36148
+ const option = options[filterMode.selectedIndex];
36149
+ if (!option)
36150
+ return;
36151
+ const selected = key.name === "space" ? !isDashboardFilterOptionSelected({ mode: filterMode, option }) : key.name === "right" || key.name === "l";
36152
+ filterMode = updateDashboardFilterSelection({ mode: filterMode, option, selected, authors });
36153
+ render();
36154
+ return;
36155
+ }
36156
+ if (key.name === "s") {
36157
+ key.preventDefault();
36158
+ filterMode = { ...filterMode, query: "", searching: true, selectedIndex: 0 };
36159
+ filterSearchComposer.textarea.setText("");
36160
+ render();
36161
+ return;
36162
+ }
36163
+ if (key.name === "return" || key.name === "enter") {
36164
+ key.preventDefault();
36165
+ const nextFilters = { statuses: [...filterMode.statuses], creators: [...filterMode.creators] };
36166
+ applyFilters(nextFilters);
36167
+ }
36168
+ return;
36169
+ }
35729
36170
  const listFocused = renderer.currentFocusedRenderable === listRegion;
35730
36171
  if (key.name === "down") {
35731
36172
  key.preventDefault();
@@ -35754,7 +36195,7 @@ async function createDashboardTui({
35754
36195
  if (listFocused && (key.name === "G" || key.name === "g" && key.shift)) {
35755
36196
  key.preventDefault();
35756
36197
  awaitingVimGoToTop = false;
35757
- moveSelectionTo(page.sessions.length - 1);
36198
+ moveSelectionTo(visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery }).length - 1);
35758
36199
  return;
35759
36200
  }
35760
36201
  if (listFocused && key.name === "g") {
@@ -35779,7 +36220,7 @@ async function createDashboardTui({
35779
36220
  awaitingVimGoToTop = false;
35780
36221
  if (key.name === "return" || key.name === "enter") {
35781
36222
  key.preventDefault();
35782
- const session = page.sessions[selectedIndex];
36223
+ const session = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery })[selectedIndex];
35783
36224
  if (session)
35784
36225
  finish({ kind: "session", sessionId: session.id });
35785
36226
  return;
@@ -35789,6 +36230,23 @@ async function createDashboardTui({
35789
36230
  finish({ kind: "new" });
35790
36231
  return;
35791
36232
  }
36233
+ if (key.name === "s") {
36234
+ key.preventDefault();
36235
+ openSessionSearch();
36236
+ return;
36237
+ }
36238
+ if (key.name === "f") {
36239
+ key.preventDefault();
36240
+ authorError = undefined;
36241
+ filterMode = createFilterMode({ kind: "status", filters });
36242
+ render();
36243
+ return;
36244
+ }
36245
+ if (key.name === "a") {
36246
+ key.preventDefault();
36247
+ openAuthorFilter();
36248
+ return;
36249
+ }
35792
36250
  if (key.name === "L" || key.shift && key.name === "l") {
35793
36251
  key.preventDefault();
35794
36252
  logoutConfirmationOpen = true;
@@ -35805,21 +36263,19 @@ async function createDashboardTui({
35805
36263
  finish(null);
35806
36264
  }
35807
36265
  };
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: "" });
36266
+ const root = new BoxRenderable2(renderer, { width: "100%", height: "100%", flexDirection: "column", paddingX: 2, overflow: "hidden" });
36267
+ const statusBand = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
36268
+ const content = new TextRenderable2(renderer, { content: "" });
35811
36269
  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
- };
36270
+ const previewBand = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
36271
+ const filterSearchRegion = new BoxRenderable2(renderer, { width: "100%", flexShrink: 0 });
36272
+ const separator = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
36273
+ const footer = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
35819
36274
  listRegion.add(content);
35820
36275
  root.add(statusBand);
35821
36276
  root.add(listRegion);
35822
36277
  root.add(previewBand);
36278
+ root.add(filterSearchRegion);
35823
36279
  root.add(separator);
35824
36280
  root.add(footer);
35825
36281
  renderer.root.add(root);
@@ -35829,8 +36285,22 @@ async function createDashboardTui({
35829
36285
  let loadingPage = false;
35830
36286
  let pageRequestInFlight = false;
35831
36287
  let pageError;
36288
+ let authorError;
36289
+ let authors = initialAuthors;
36290
+ let authorsLoaded = loadAuthors === undefined || initialAuthors.length > 0;
36291
+ let loadingAuthors = false;
35832
36292
  let awaitingVimGoToTop = false;
35833
36293
  let logoutConfirmationOpen = false;
36294
+ let filters = initialFilters;
36295
+ let filterMode;
36296
+ let sessionSearchOpen = false;
36297
+ let sessionSearchQuery = "";
36298
+ root.onMouseDown = (event) => {
36299
+ if (filterMode?.searching || sessionSearchOpen)
36300
+ return;
36301
+ event.preventDefault();
36302
+ listRegion.focus();
36303
+ };
35834
36304
  let destroyed = false;
35835
36305
  let rendererDestroyPromise;
35836
36306
  let settled = false;
@@ -35840,10 +36310,41 @@ async function createDashboardTui({
35840
36310
  let rejectDraft = () => {
35841
36311
  return;
35842
36312
  };
36313
+ let desiredContentScrollY = 0;
35843
36314
  const action = new Promise((resolve, reject) => {
35844
36315
  resolveAction = resolve;
35845
36316
  rejectDraft = reject;
35846
36317
  });
36318
+ let mountedSearchComposer;
36319
+ const filterSearchComposer = createComposer({
36320
+ renderer,
36321
+ parent: filterSearchRegion,
36322
+ tag: "Filter",
36323
+ placeholder: "Filter options\u2026",
36324
+ screenPaddingX: 2,
36325
+ onSubmit: () => applyFilterSearch(),
36326
+ onContentChange: () => {
36327
+ if (!filterMode?.searching)
36328
+ return;
36329
+ filterMode = { ...filterMode, query: filterSearchComposer.textarea.plainText, selectedIndex: 0 };
36330
+ render();
36331
+ }
36332
+ });
36333
+ const sessionSearchComposer = createComposer({
36334
+ renderer,
36335
+ parent: filterSearchRegion,
36336
+ tag: "Search",
36337
+ placeholder: "Search by ID, title, or number\u2026",
36338
+ screenPaddingX: 2,
36339
+ onSubmit: () => applySessionSearch(),
36340
+ onContentChange: () => {
36341
+ if (!sessionSearchOpen)
36342
+ return;
36343
+ sessionSearchQuery = sessionSearchComposer.textarea.plainText;
36344
+ selectedIndex = 0;
36345
+ render();
36346
+ }
36347
+ });
35847
36348
  async function loadAdjacentPage(direction) {
35848
36349
  if (pageRequestInFlight)
35849
36350
  return;
@@ -35860,7 +36361,8 @@ async function createDashboardTui({
35860
36361
  return;
35861
36362
  page = loadedPage;
35862
36363
  pageIndex = Math.max(1, pageIndex + (direction === "next" ? 1 : -1));
35863
- selectedIndex = direction === "next" ? 0 : Math.max(0, page.sessions.length - 1);
36364
+ const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
36365
+ selectedIndex = direction === "next" ? 0 : Math.max(0, sessions.length - 1);
35864
36366
  } catch (error93) {
35865
36367
  if (!destroyed && !settled)
35866
36368
  pageError = error93 instanceof Error ? error93.message : String(error93);
@@ -35879,7 +36381,7 @@ async function createDashboardTui({
35879
36381
  const refreshedPage = await loadPage({ target: "current", signal: loadPageAbortController.signal });
35880
36382
  if (destroyed || settled)
35881
36383
  return;
35882
- const selectedSessionId = page.sessions[selectedIndex]?.id;
36384
+ const selectedSessionId = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery })[selectedIndex]?.id;
35883
36385
  page = refreshedPage;
35884
36386
  const refreshedSelectedIndex = selectedSessionId ? page.sessions.findIndex((session) => session.id === selectedSessionId) : -1;
35885
36387
  selectedIndex = refreshedSelectedIndex >= 0 ? refreshedSelectedIndex : Math.max(0, Math.min(selectedIndex, page.sessions.length - 1));
@@ -35894,10 +36396,53 @@ async function createDashboardTui({
35894
36396
  pageRequestInFlight = false;
35895
36397
  }
35896
36398
  }
36399
+ async function applyFilters(nextFilters) {
36400
+ if (loadingPage)
36401
+ return;
36402
+ loadingPage = true;
36403
+ pageError = undefined;
36404
+ authorError = undefined;
36405
+ render();
36406
+ try {
36407
+ page = await loadPage({ filters: nextFilters, signal: loadPageAbortController.signal });
36408
+ filters = nextFilters;
36409
+ pageIndex = 1;
36410
+ selectedIndex = 0;
36411
+ filterMode = undefined;
36412
+ } catch (error93) {
36413
+ pageError = error93 instanceof Error ? error93.message : String(error93);
36414
+ } finally {
36415
+ loadingPage = false;
36416
+ render();
36417
+ }
36418
+ }
36419
+ async function openAuthorFilter() {
36420
+ filterMode = createFilterMode({ kind: "creator", filters });
36421
+ authorError = undefined;
36422
+ render();
36423
+ if (authorsLoaded || loadingAuthors || !loadAuthors)
36424
+ return;
36425
+ loadingAuthors = true;
36426
+ render();
36427
+ try {
36428
+ authors = await loadAuthors();
36429
+ authorsLoaded = true;
36430
+ if (filterMode?.kind === "creator") {
36431
+ const firstSelectedIndex = authors.findIndex((author) => filterMode?.creators.some((creator) => creator.id === author.id));
36432
+ filterMode = { ...filterMode, selectedIndex: Math.max(0, firstSelectedIndex) };
36433
+ }
36434
+ } catch (error93) {
36435
+ authorError = error93 instanceof Error ? error93.message : String(error93);
36436
+ } finally {
36437
+ loadingAuthors = false;
36438
+ render();
36439
+ }
36440
+ }
35897
36441
  async function moveSelection(direction) {
35898
- if (loadingPage || page.sessions.length === 0)
36442
+ const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
36443
+ if (loadingPage || sessions.length === 0)
35899
36444
  return;
35900
- const isBoundary = direction === "next" ? selectedIndex === page.sessions.length - 1 : selectedIndex === 0;
36445
+ const isBoundary = direction === "next" ? selectedIndex === sessions.length - 1 : selectedIndex === 0;
35901
36446
  if (!isBoundary) {
35902
36447
  selectedIndex += direction === "next" ? 1 : -1;
35903
36448
  render();
@@ -35912,7 +36457,13 @@ async function createDashboardTui({
35912
36457
  }
35913
36458
  return false;
35914
36459
  };
35915
- keyHandler = handleSessionKey;
36460
+ filterSearchComposer.textarea.onKeyDown = handleSessionKey;
36461
+ sessionSearchComposer.textarea.onKeyDown = handleSessionKey;
36462
+ keyHandler = (key) => {
36463
+ if (mountedSearchComposer && key.name !== "escape" && !(key.ctrl && key.name === "c"))
36464
+ return;
36465
+ handleSessionKey(key);
36466
+ };
35916
36467
  renderer.addInputHandler(inputHandler);
35917
36468
  renderer.keyInput.on("keypress", keyHandler);
35918
36469
  renderer.on(CliRenderEvents2.RENDER_ERROR, (event) => fail(event.error));
@@ -35939,6 +36490,10 @@ async function createDashboardTui({
35939
36490
  if (resizeHandler)
35940
36491
  renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
35941
36492
  stopPageRefresh();
36493
+ if (contentScrollHandler)
36494
+ renderer.off(CliRenderEvents2.FRAME, contentScrollHandler);
36495
+ filterSearchComposer.destroy();
36496
+ sessionSearchComposer.destroy();
35942
36497
  rendererDestroyed2 = true;
35943
36498
  renderer.destroy();
35944
36499
  },
@@ -35956,6 +36511,8 @@ async function createDashboardTui({
35956
36511
  if (refreshTimer !== undefined)
35957
36512
  clearInterval(refreshTimer);
35958
36513
  loadPageAbortController.abort();
36514
+ if (contentScrollHandler)
36515
+ renderer.off(CliRenderEvents2.FRAME, contentScrollHandler);
35959
36516
  if (!rendererDestroyed2) {
35960
36517
  rendererDestroyed2 = true;
35961
36518
  renderer.destroy();
@@ -35993,6 +36550,63 @@ function rowsBelowVisibleWindow({ sessionCount, selectedIndex, height }) {
35993
36550
  const { firstIndex, rowCount } = visibleListWindow({ sessionCount, selectedIndex, height });
35994
36551
  return Math.max(0, sessionCount - (firstIndex + rowCount));
35995
36552
  }
36553
+ function createFilterMode({ kind, filters }) {
36554
+ return {
36555
+ kind,
36556
+ selectedIndex: 0,
36557
+ statuses: [...filters.statuses],
36558
+ creators: [...filters.creators],
36559
+ query: "",
36560
+ searching: false
36561
+ };
36562
+ }
36563
+ function formatFilterMenu({ mode, authors, width }) {
36564
+ const options = visibleDashboardFilterOptions({ mode, authors });
36565
+ const selectedCount = mode.kind === "status" ? mode.statuses.length : mode.creators.length;
36566
+ const heading = `${mode.kind === "status" ? "Filter by status" : "Filter by author"} \xB7 ${selectedCount} selected \xB7 empty shows all`;
36567
+ const rows = options.map((option, index) => {
36568
+ const active = isDashboardFilterOptionSelected({ mode, option });
36569
+ const label = `${active ? "[x]" : "[ ]"} ${truncate(option.label, Math.max(1, width - 4))}`;
36570
+ const chunk = index === mode.selectedIndex ? bg(PALETTE.selectionBg)(fg3(PALETTE.bodyText)(label.padEnd(width))) : active ? bold2(fg3(PALETTE.humanAccent)(label)) : fg3(PALETTE.dimText)(label);
36571
+ return [chunk];
36572
+ });
36573
+ return joinStyled([
36574
+ [bold2(fg3(PALETTE.bodyText)(truncate(heading, width)))],
36575
+ ...rows.length > 0 ? rows : [[fg3(PALETTE.dimText)("No matching options.")]]
36576
+ ], `
36577
+ `);
36578
+ }
36579
+ function dashboardFilterOptions({ kind, authors }) {
36580
+ 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 }));
36581
+ }
36582
+ function visibleDashboardFilterOptions({ mode, authors }) {
36583
+ return dashboardFilterOptions({ kind: mode.kind, authors }).filter((option) => fuzzyMatches(option.label, mode.query));
36584
+ }
36585
+ function isDashboardFilterOptionSelected({ mode, option }) {
36586
+ return option.kind === "status" ? mode.statuses.includes(option.id) : mode.creators.some((creator) => creator.id === option.id);
36587
+ }
36588
+ function updateDashboardFilterSelection({ mode, option, selected, authors }) {
36589
+ if (option.kind === "status") {
36590
+ const selectedStatuses = new Set(mode.statuses);
36591
+ if (selected)
36592
+ selectedStatuses.add(option.id);
36593
+ else
36594
+ selectedStatuses.delete(option.id);
36595
+ return {
36596
+ ...mode,
36597
+ statuses: lifecycleStatusOptions.map((entry) => entry.value).filter((status) => selectedStatuses.has(status))
36598
+ };
36599
+ }
36600
+ const selectedCreatorIds = new Set(mode.creators.map((creator) => creator.id));
36601
+ if (selected)
36602
+ selectedCreatorIds.add(option.id);
36603
+ else
36604
+ selectedCreatorIds.delete(option.id);
36605
+ return {
36606
+ ...mode,
36607
+ creators: authors.filter((author) => selectedCreatorIds.has(author.id))
36608
+ };
36609
+ }
35996
36610
  function formatSessionList({ sessions, selectedIndex, width, height }) {
35997
36611
  const { firstIndex, rowCount } = visibleListWindow({ sessionCount: sessions.length, selectedIndex, height });
35998
36612
  const visibleSessions = sessions.slice(firstIndex, firstIndex + rowCount);
@@ -36079,18 +36693,34 @@ function formatUpdatedAt(value) {
36079
36693
  return `${Math.floor(seconds / (60 * 60))}h ago`;
36080
36694
  return new Date(milliseconds).toLocaleDateString(undefined, { month: "short", day: "numeric" });
36081
36695
  }
36082
- function dashboardStatusBand({ page, pageIndex }) {
36083
- const openCount = page.sessions.filter((session) => session.status === "open").length;
36696
+ function matchesSessionSearch({ session, query }) {
36697
+ return fuzzyMatches(session.id, query) || fuzzyMatches(session.title ?? "", query) || fuzzyMatches(String(session.sessionNumber), query);
36698
+ }
36699
+ function visibleDashboardSessions({ sessions, query }) {
36700
+ return sessions.filter((session) => matchesSessionSearch({ session, query }));
36701
+ }
36702
+ function dashboardStatusBand({ page, pageIndex, filters, sessionSearchQuery }) {
36703
+ const openCount = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery }).filter((session) => session.status === "open").length;
36084
36704
  const paging = [`page ${pageIndex}`];
36085
36705
  if (page.canGoPrevious)
36086
36706
  paging.push("\u2190 prev");
36087
36707
  if (page.hasMore)
36088
36708
  paging.push("more \u2192");
36089
- return `remy \xB7 sessions \xB7 ${openCount} open \xB7 ${paging.join(" \xB7 ")}`;
36709
+ const activeFilters = [
36710
+ filters.statuses.length > 0 ? `status: ${filters.statuses.map(sessionStatusLabel).join(", ")}` : undefined,
36711
+ filters.creators.length > 0 ? `author: ${filters.creators.map((creator) => creator.name).join(", ")}` : undefined,
36712
+ sessionSearchQuery ? `search: ${sessionSearchQuery}` : undefined
36713
+ ].filter((value) => value !== undefined);
36714
+ return `remy \xB7 sessions \xB7 ${openCount} open${activeFilters.length > 0 ? ` \xB7 ${activeFilters.join(" \xB7 ")}` : ""} \xB7 ${paging.join(" \xB7 ")}`;
36715
+ }
36716
+ function hasActiveFilters(filters) {
36717
+ return filters.statuses.length > 0 || filters.creators.length > 0;
36090
36718
  }
36091
- function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen }) {
36719
+ function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen, sessionSearchOpen }) {
36092
36720
  if (logoutConfirmationOpen)
36093
36721
  return "Log out of Remy? y confirms \xB7 Esc cancels";
36722
+ if (sessionSearchOpen)
36723
+ return "type to search \xB7 \u23CE apply search \xB7 Esc clear search";
36094
36724
  const parts = [];
36095
36725
  if (loadingPage)
36096
36726
  parts.push("Loading sessions\u2026");
@@ -36098,9 +36728,22 @@ function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmation
36098
36728
  parts.push(`Could not load sessions: ${pageError}`);
36099
36729
  if (rowsBelow > 0)
36100
36730
  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");
36731
+ 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
36732
  return parts.join(" \xB7 ");
36103
36733
  }
36734
+ function dashboardFilterFooter({ mode, loadingPage, loadingAuthors, pageError, authorError }) {
36735
+ if (loadingAuthors)
36736
+ return "Loading authors\u2026 \xB7 Esc cancel";
36737
+ if (loadingPage)
36738
+ return "Applying filter\u2026";
36739
+ if (mode.searching)
36740
+ return "type to filter \xB7 \u23CE apply search \xB7 Esc cancel search";
36741
+ return [
36742
+ authorError ? `Could not load authors: ${authorError}` : undefined,
36743
+ pageError ? `Could not apply filter: ${pageError}` : undefined,
36744
+ "\u2191\u2193/jk move \xB7 space toggle \xB7 \u2190\u2192/hl mark \xB7 s search \xB7 \u23CE apply \xB7 Esc cancel"
36745
+ ].filter((value) => value !== undefined).join(" \xB7 ");
36746
+ }
36104
36747
  function truncate(value, width) {
36105
36748
  if (value.length <= width)
36106
36749
  return value;
@@ -36111,87 +36754,33 @@ async function createDefaultRenderer() {
36111
36754
  }
36112
36755
 
36113
36756
  // 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";
36757
+ 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
36758
 
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);
36759
+ // src/tui/attachment-presentation.ts
36760
+ import { dim as dim3, fg as fg4, StyledText as StyledText3 } from "@opentui/core";
36761
+ function attachmentUploadMessage(filenames) {
36762
+ return filenames.length === 1 ? `Attaching ${filenames[0]}\u2026` : `Attaching ${filenames.length} files\u2026`;
36763
+ }
36764
+ function renderAttachmentFeedback(feedback) {
36765
+ const presentation = feedback.kind === "progress" ? { glyph: "\u28FF", color: PALETTE.progress } : feedback.kind === "success" ? { glyph: "\u2713", color: PALETTE.statusCompleted } : { glyph: "\u2717", color: PALETTE.failure };
36766
+ return new StyledText3([
36767
+ fg4(presentation.color)(`${presentation.glyph} `),
36768
+ fg4(feedback.kind === "failure" ? PALETTE.failure : PALETTE.bodyText)(feedback.message)
36769
+ ]);
36123
36770
  }
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
- };
36771
+ function renderAttachmentSummary(attachments) {
36772
+ if (attachments.length === 0)
36773
+ return new StyledText3([]);
36774
+ const label = attachments.length === 1 ? "Attachment" : `${attachments.length} attachments`;
36775
+ return new StyledText3([
36776
+ dim3(fg4(PALETTE.dimText)("\u21B3 ")),
36777
+ fg4(PALETTE.tool)(label),
36778
+ dim3(fg4(PALETTE.dimText)(` \xB7 ${attachments.map((attachment) => attachment.filename).join(" \xB7 ")}`))
36779
+ ]);
36191
36780
  }
36192
36781
 
36193
36782
  // src/tui/path-completion-menu.ts
36194
- import { bg as bg2, fg as fg4, stringToStyledText as stringToStyledText3, StyledText as StyledText3 } from "@opentui/core";
36783
+ import { bg as bg2, fg as fg5, stringToStyledText as stringToStyledText3, StyledText as StyledText4 } from "@opentui/core";
36195
36784
  var maximumVisibleCompletions = 8;
36196
36785
  function renderPathCompletionMenu({ completions, selectedIndex }) {
36197
36786
  if (completions.length === 0)
@@ -36202,7 +36791,7 @@ function renderPathCompletionMenu({ completions, selectedIndex }) {
36202
36791
  const completionIndex = firstIndex + index;
36203
36792
  const selected = completionIndex === selectedIndex;
36204
36793
  const rowStyle = selected ? bg2(PALETTE.selectionBg) : (chunk) => chunk;
36205
- return new StyledText3([rowStyle(fg4(PALETTE.bodyText)(`${selected ? "\u203A " : " "}${completion.display}`))]);
36794
+ return new StyledText4([rowStyle(fg5(PALETTE.bodyText)(`${selected ? "\u203A " : " "}${completion.display}`))]);
36206
36795
  });
36207
36796
  const overflow = completions.length > visibleCompletions.length ? stringToStyledText3(`${firstIndex + 1}-${firstIndex + visibleCompletions.length} of ${completions.length}`) : undefined;
36208
36797
  return joinStyled([
@@ -36242,13 +36831,62 @@ async function completePathToken({ text, cwd, homeDirectory = homedir() }) {
36242
36831
  const normalized = replacementPath.split(sep).join("/");
36243
36832
  const suffix = entry.isDirectory() ? "/" : "";
36244
36833
  const completed = `${normalized}${suffix}`;
36245
- return { display: completed, replacement: `${token.startsWith("@") ? "@" : ""}${completed}` };
36834
+ return {
36835
+ display: completed,
36836
+ replacement: `${token.startsWith("@") ? "@" : ""}${completed}`,
36837
+ ...entry.isFile() ? { attachmentPath: completedPath } : {}
36838
+ };
36246
36839
  });
36247
36840
  }
36248
36841
  function replaceActivePathToken({ text, replacement }) {
36249
36842
  const token = activePathToken(text);
36250
36843
  return token === undefined ? text : `${text.slice(0, text.length - token.length)}${replacement}`;
36251
36844
  }
36845
+ function selectedAttachmentPaths({ text, selections }) {
36846
+ return selections.filter((selection) => text.slice(selection.start, selection.end) === selection.token && hasPathTokenBoundaries({ text, selection })).map((selection) => selection.attachmentPath);
36847
+ }
36848
+ function createSelectedPathCompletion({ attachmentPath, token, end }) {
36849
+ return { attachmentPath, token, start: end - token.length, end };
36850
+ }
36851
+ function reconcileSelectedPathCompletions({
36852
+ previousText,
36853
+ text,
36854
+ selections
36855
+ }) {
36856
+ if (text === previousText)
36857
+ return selections;
36858
+ let changeStart = 0;
36859
+ while (changeStart < previousText.length && changeStart < text.length && previousText[changeStart] === text[changeStart])
36860
+ changeStart += 1;
36861
+ let previousChangeEnd = previousText.length;
36862
+ let nextChangeEnd = text.length;
36863
+ while (previousChangeEnd > changeStart && nextChangeEnd > changeStart && previousText[previousChangeEnd - 1] === text[nextChangeEnd - 1]) {
36864
+ previousChangeEnd -= 1;
36865
+ nextChangeEnd -= 1;
36866
+ }
36867
+ const offsetChange = nextChangeEnd - previousChangeEnd;
36868
+ return selections.flatMap((selection) => {
36869
+ if (selection.end <= changeStart)
36870
+ return [selection];
36871
+ if (selection.start >= previousChangeEnd) {
36872
+ return [{
36873
+ ...selection,
36874
+ start: selection.start + offsetChange,
36875
+ end: selection.end + offsetChange
36876
+ }];
36877
+ }
36878
+ return [];
36879
+ }).filter((selection) => hasPathTokenBoundaries({ text, selection }));
36880
+ }
36881
+ function hasPathTokenBoundaries({ text, selection }) {
36882
+ let before = selection.start;
36883
+ while (before > 0 && "([{'\"`".includes(text[before - 1]))
36884
+ before -= 1;
36885
+ let after = selection.end;
36886
+ while (after < text.length && '),.;:!?]}"`'.includes(text[after]))
36887
+ after += 1;
36888
+ return (before === 0 || /\s/.test(text[before - 1])) && (after === text.length || /\s/.test(text[after]));
36889
+ }
36252
36890
  function activePathToken(text) {
36253
36891
  const match = text.match(/(?:^|\s)(\S*)$/);
36254
36892
  return match?.[1];
@@ -36261,6 +36899,7 @@ async function createNewSessionWizard({
36261
36899
  repositories,
36262
36900
  reloadRepositories,
36263
36901
  suggestRepositories,
36902
+ suggestBranches,
36264
36903
  readClipboardImage,
36265
36904
  savePastedImage,
36266
36905
  attachPromptPaths,
@@ -36297,15 +36936,19 @@ async function createNewSessionWizard({
36297
36936
  else
36298
36937
  overrides.set(repositoryId, isSelected);
36299
36938
  }, renderRepositoryLoadingSkeleton = function() {
36300
- return new StyledText4([
36301
- dim3(fg5(PALETTE.dimText)(`\u283F \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
36939
+ return new StyledText5([
36940
+ dim4(fg6(PALETTE.dimText)(`\u283F \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
36302
36941
  \u283F \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
36303
36942
  \u283F \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588`))
36304
36943
  ]);
36305
36944
  }, render = function() {
36306
36945
  const repositoryListVisible = step === "repositories" || step === "repositorySearch";
36307
36946
  const composerVisible = step === "prompt" || step === "repositorySearch" || step === "recompute";
36308
- const orient = (body) => joinStyled([renderStepIndicator({ step, hasInstallationChoice: installationIds.length > 1 }), body], `
36947
+ const orient = (body) => joinStyled([renderStepIndicator({
36948
+ step,
36949
+ hasInstallationChoice: installationIds.length > 1,
36950
+ hasBranchChoice: step === "loadingBranches" || branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch")
36951
+ }), body], `
36309
36952
  `);
36310
36953
  if (composerVisible && !composerMounted) {
36311
36954
  composerMounted = true;
@@ -36323,19 +36966,22 @@ async function createNewSessionWizard({
36323
36966
  repositoryRows.content = "";
36324
36967
  repositoryDivider.content = "";
36325
36968
  repositoryFooter.content = "";
36326
- const attachmentText = attachments.length > 0 ? stringToStyledText4(`Attachments: ${attachments.map((attachment) => attachment.filename).join(", ")}`) : undefined;
36969
+ const attachmentText = renderAttachmentSummary(attachments);
36970
+ const attachmentStatus = attachmentFeedback ? renderAttachmentFeedback(attachmentFeedback) : undefined;
36327
36971
  if (step === "prompt") {
36328
36972
  const parts = [
36329
- new StyledText4([stepHeader("New remote Codex session")]),
36973
+ new StyledText5([stepHeader("New remote Codex session")]),
36330
36974
  stringToStyledText4("Describe the work you want Remy to do. Press Enter to continue; Esc back.")
36331
36975
  ];
36332
- if (attachmentText)
36976
+ if (attachmentText.chunks.length > 0)
36333
36977
  parts.push(attachmentText);
36334
36978
  const completionMenu = pathCompletionMenuOpen ? renderPathCompletionMenu({ completions: pathCompletions, selectedIndex: completionIndex }) : undefined;
36335
36979
  if (completionMenu)
36336
36980
  parts.push(completionMenu);
36337
36981
  if (status)
36338
36982
  parts.push(stringToStyledText4(status));
36983
+ if (attachmentStatus)
36984
+ parts.push(attachmentStatus);
36339
36985
  content.content = orient(joinStyled(parts, `
36340
36986
 
36341
36987
  `));
@@ -36343,7 +36989,7 @@ async function createNewSessionWizard({
36343
36989
  const rows = joinStyled(installationIds.map((id, index) => renderSelectableRow({ label: id, isCursor: index === installationIndex })), `
36344
36990
  `);
36345
36991
  content.content = orient(joinStyled([
36346
- new StyledText4([stepHeader("Select GitHub installation")]),
36992
+ new StyledText5([stepHeader("Select GitHub installation")]),
36347
36993
  rows,
36348
36994
  stringToStyledText4("\u2191\u2193 move \xB7 \u23CE continue \xB7 esc back")
36349
36995
  ], `
@@ -36352,11 +36998,11 @@ async function createNewSessionWizard({
36352
36998
  } else if (step === "loadingRepositories") {
36353
36999
  const failed = repositoryLoadState === "failed";
36354
37000
  const parts = [
36355
- new StyledText4([stepHeader("Select repositories")]),
37001
+ new StyledText5([stepHeader("Select repositories")]),
36356
37002
  stringToStyledText4(failed ? "Could not load your repositories." : "Loading your repos\u2026")
36357
37003
  ];
36358
37004
  if (failed) {
36359
- parts.push(new StyledText4([dim3(fg5(PALETTE.dimText)(`(${status})`))]));
37005
+ parts.push(new StyledText5([dim4(fg6(PALETTE.dimText)(`(${status})`))]));
36360
37006
  parts.push(stringToStyledText4("r retry \xB7 esc back"));
36361
37007
  } else {
36362
37008
  parts.push(renderRepositoryLoadingSkeleton());
@@ -36367,12 +37013,41 @@ async function createNewSessionWizard({
36367
37013
  `));
36368
37014
  } else if (step === "loadingSuggestions") {
36369
37015
  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."))]),
37016
+ new StyledText5([stepHeader("Select repositories")]),
37017
+ new StyledText5([fg6(PALETTE.progress)(`${suggestionSpinnerFrames[suggestionSpinnerFrame]} Remy is matching your prompt to repositories\u2026`)]),
37018
+ new StyledText5([dim4(fg6(PALETTE.dimText)("This can take a few seconds."))]),
36373
37019
  stringToStyledText4("s select repositories yourself \xB7 esc back")
36374
37020
  ], `
36375
37021
 
37022
+ `));
37023
+ } else if (step === "loadingBranches") {
37024
+ content.content = orient(joinStyled([
37025
+ new StyledText5([stepHeader("Choose branches")]),
37026
+ new StyledText5([fg6(PALETTE.progress)(`${suggestionSpinnerFrames[suggestionSpinnerFrame]} Remy is checking for related open pull requests\u2026`)]),
37027
+ new StyledText5([dim4(fg6(PALETTE.dimText)("This can take a few seconds."))]),
37028
+ stringToStyledText4("esc back")
37029
+ ], `
37030
+
37031
+ `));
37032
+ } else if (step === "branches") {
37033
+ const existing = branchSuggestions.filter((suggestion) => suggestion.suggestionKind !== "createSessionBranch");
37034
+ const parts = [new StyledText5([stepHeader("Choose branches")])];
37035
+ if (existing.length > 0) {
37036
+ parts.push(stringToStyledText4("Remy found related open pull-request work:"));
37037
+ for (const suggestion of existing) {
37038
+ const repository = installationRepositories().find((candidate) => candidate.id === suggestion.repositoryId);
37039
+ parts.push(stringToStyledText4(`${repository?.fullName ?? suggestion.repositoryId} \u2014 ${suggestion.branchName}${suggestion.pullRequest ? ` (PR #${suggestion.pullRequest.number}: ${suggestion.pullRequest.title})` : ""}`));
37040
+ }
37041
+ parts.push(new StyledText5(renderSelectableRow({ label: "Continue from the suggested branch", isCursor: branchChoice === "existing" })));
37042
+ parts.push(new StyledText5(renderSelectableRow({ label: "Create a new session branch", isCursor: branchChoice === "new" })));
37043
+ parts.push(stringToStyledText4("\u2191\u2193 choose \xB7 \u23CE continue \xB7 esc back"));
37044
+ } else {
37045
+ parts.push(stringToStyledText4("Remy recommends creating a new session branch."));
37046
+ parts.push(new StyledText5([dim4(fg6(PALETTE.dimText)(status || "No related open pull request was identified from your request."))]));
37047
+ parts.push(stringToStyledText4("\u23CE continue \xB7 esc back"));
37048
+ }
37049
+ content.content = orient(joinStyled(parts, `
37050
+
36376
37051
  `));
36377
37052
  } else if (repositoryListVisible) {
36378
37053
  const selectionOverrides = searchSelections ?? manualSelections;
@@ -36387,15 +37062,17 @@ async function createNewSessionWizard({
36387
37062
  `) : step === "repositorySearch" ? "No matching repositories." : "No repositories available for this installation.";
36388
37063
  repositoryDivider.content = renderComposerDivider({ width: renderer.width - 2 });
36389
37064
  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")]),
37065
+ const parts = step === "repositorySearch" ? [new StyledText5([stepHeader("Search repositories")]), stringToStyledText4("Fuzzy matching repository names.")] : [
37066
+ new StyledText5([stepHeader("Select repositories")]),
36392
37067
  stringToStyledText4(`${selected.size} selected \xB7 ${suggestedIds.size} suggested by Remy${suggestionConfidence ? ` (${suggestionConfidence} confidence)` : ""}`),
36393
37068
  stringToStyledText4("Suggestions are based on your prompt and recompute instruction.")
36394
37069
  ];
36395
- if (attachmentText)
37070
+ if (attachmentText.chunks.length > 0)
36396
37071
  parts.push(attachmentText);
36397
37072
  if (status && step === "repositories")
36398
37073
  parts.push(stringToStyledText4(status));
37074
+ if (attachmentStatus)
37075
+ parts.push(attachmentStatus);
36399
37076
  content.content = orient(joinStyled(parts, `
36400
37077
 
36401
37078
  `));
@@ -36403,11 +37080,11 @@ async function createNewSessionWizard({
36403
37080
  editor.placeholder = "Filter repositories\u2026";
36404
37081
  } else if (step === "recompute") {
36405
37082
  const parts = [
36406
- new StyledText4([stepHeader("Recompute suggestions")]),
37083
+ new StyledText5([stepHeader("Recompute suggestions")]),
36407
37084
  stringToStyledText4("Suggestions use your prompt and instruction. Existing manual toggles stay selected or unselected."),
36408
37085
  stringToStyledText4("Enter instruction, then press Enter to continue. Esc back.")
36409
37086
  ];
36410
- if (attachmentText)
37087
+ if (attachmentText.chunks.length > 0)
36411
37088
  parts.push(attachmentText);
36412
37089
  content.content = orient(joinStyled(parts, `
36413
37090
 
@@ -36415,26 +37092,31 @@ async function createNewSessionWizard({
36415
37092
  editor.placeholder = "e.g. worker, migration, dashboard";
36416
37093
  } else {
36417
37094
  const selected = installationRepositories().filter((repository) => selectedRepositoryIds().has(repository.id));
36418
- const meta5 = new StyledText4([
36419
- fg5(PALETTE.bodyText)(`Installation: ${selectedInstallationId() ?? "(missing)"}
37095
+ const branchOverrides = branchChoice === "existing" ? branchSuggestions.filter((suggestion) => suggestion.suggestionKind !== "createSessionBranch") : [];
37096
+ const meta5 = new StyledText5([
37097
+ fg6(PALETTE.bodyText)(`Installation: ${selectedInstallationId() ?? "(missing)"}
37098
+ `),
37099
+ fg6(PALETTE.bodyText)(`Repositories: ${selected.map((repository) => repository.fullName).join(", ") || "(none)"}
36420
37100
  `),
36421
- fg5(PALETTE.bodyText)(`Repositories: ${selected.map((repository) => repository.fullName).join(", ") || "(none)"}
37101
+ fg6(PALETTE.bodyText)(`Branches: ${branchOverrides.length > 0 ? branchOverrides.map((override) => override.branchName).join(", ") : "new session branches"}
36422
37102
  `),
36423
- fg5(PALETTE.bodyText)(`Model: ${newSessionModelLabel(model)} \xB7 ${newSessionReasoningEffortLabel(reasoningEffort)} reasoning`),
36424
- dim3(fg5(PALETTE.dimText)(" \u25B8 m model \xB7 \u2190\u2192 reasoning"))
37103
+ fg6(PALETTE.bodyText)(`Model: ${newSessionModelLabel(model)} \xB7 ${newSessionReasoningEffortLabel(reasoningEffort)} reasoning`),
37104
+ dim4(fg6(PALETTE.dimText)(" \u25B8 m model \xB7 \u2190\u2192 reasoning"))
36425
37105
  ]);
36426
37106
  const parts = [
36427
- new StyledText4([stepHeader("Confirm new session")]),
36428
- new StyledText4([renderRoleLabel({ label: "You", role: "human" })]),
37107
+ new StyledText5([stepHeader("Confirm new session")]),
37108
+ new StyledText5([renderRoleLabel({ label: "You", role: "human" })]),
36429
37109
  renderMessageBody({ text: prompt, role: "human" }),
36430
37110
  meta5
36431
37111
  ];
36432
- if (attachmentText)
37112
+ if (attachmentText.chunks.length > 0)
36433
37113
  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"));
37114
+ 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."))]));
37115
+ 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
37116
  if (status)
36437
37117
  parts.push(stringToStyledText4(status));
37118
+ if (attachmentStatus)
37119
+ parts.push(attachmentStatus);
36438
37120
  content.content = orient(joinStyled(parts, `
36439
37121
 
36440
37122
  `));
@@ -36459,7 +37141,7 @@ async function createNewSessionWizard({
36459
37141
  syncSuggestionLoadingIndicator();
36460
37142
  renderer.requestRender();
36461
37143
  }, syncSuggestionLoadingIndicator = function() {
36462
- if (destroyed || settled || step !== "loadingSuggestions") {
37144
+ if (destroyed || settled || step !== "loadingSuggestions" && step !== "loadingBranches") {
36463
37145
  if (suggestionSpinnerTimer) {
36464
37146
  clearInterval(suggestionSpinnerTimer);
36465
37147
  suggestionSpinnerTimer = undefined;
@@ -36470,22 +37152,28 @@ async function createNewSessionWizard({
36470
37152
  if (suggestionSpinnerTimer)
36471
37153
  return;
36472
37154
  suggestionSpinnerTimer = setInterval(() => {
36473
- if (destroyed || settled || step !== "loadingSuggestions") {
37155
+ if (destroyed || settled || step !== "loadingSuggestions" && step !== "loadingBranches") {
36474
37156
  syncSuggestionLoadingIndicator();
36475
37157
  return;
36476
37158
  }
36477
37159
  suggestionSpinnerFrame = (suggestionSpinnerFrame + 1) % suggestionSpinnerFrames.length;
36478
37160
  render();
36479
37161
  }, suggestionSpinnerIntervalMs);
37162
+ }, invalidatePendingConfirmation = function() {
37163
+ pendingConfirmation = undefined;
37164
+ if (attachmentFeedback?.kind === "progress")
37165
+ attachmentFeedback = undefined;
36480
37166
  }, finish = function(value) {
36481
37167
  if (settled)
36482
37168
  return;
37169
+ invalidatePendingConfirmation();
36483
37170
  settled = true;
36484
37171
  syncSuggestionLoadingIndicator();
36485
37172
  resolveDraft(value);
36486
37173
  }, fail = function(error93) {
36487
37174
  if (settled)
36488
37175
  return;
37176
+ invalidatePendingConfirmation();
36489
37177
  settled = true;
36490
37178
  syncSuggestionLoadingIndicator();
36491
37179
  rejectDraft(error93);
@@ -36556,7 +37244,15 @@ async function createNewSessionWizard({
36556
37244
  returnToPromptFromRepositoryLoading();
36557
37245
  return;
36558
37246
  }
37247
+ if (step === "loadingBranches" || step === "branches") {
37248
+ branchRequestGeneration += 1;
37249
+ step = "repositories";
37250
+ render();
37251
+ return;
37252
+ }
36559
37253
  if (step === "recompute" || step === "confirmation") {
37254
+ if (step === "confirmation")
37255
+ invalidatePendingConfirmation();
36560
37256
  step = "repositories";
36561
37257
  editor.setText("");
36562
37258
  render();
@@ -36584,6 +37280,15 @@ async function createNewSessionWizard({
36584
37280
  const completedText = replaceActivePathToken({ text: editor.plainText, replacement: completion.replacement });
36585
37281
  editor.setText(completedText);
36586
37282
  editor.cursorOffset = completedText.length;
37283
+ if (completion.attachmentPath) {
37284
+ const selectedCompletion = createSelectedPathCompletion({
37285
+ attachmentPath: completion.attachmentPath,
37286
+ token: completion.replacement,
37287
+ end: completedText.length
37288
+ });
37289
+ selectedPathCompletions = [...selectedPathCompletions, selectedCompletion];
37290
+ }
37291
+ previousPromptEditorText = completedText;
36587
37292
  }
36588
37293
  pathCompletions = [];
36589
37294
  pathCompletionMenuOpen = false;
@@ -36609,12 +37314,14 @@ async function createNewSessionWizard({
36609
37314
  if (step === "confirmation") {
36610
37315
  if (key.name === "m") {
36611
37316
  key.preventDefault();
37317
+ invalidatePendingConfirmation();
36612
37318
  model = cycle({ values: newSessionModels, value: model, direction: 1 });
36613
37319
  render();
36614
37320
  return;
36615
37321
  }
36616
37322
  if (key.name === "left" || key.name === "right") {
36617
37323
  key.preventDefault();
37324
+ invalidatePendingConfirmation();
36618
37325
  reasoningEffort = clampedStep({
36619
37326
  values: newSessionReasoningEfforts,
36620
37327
  value: reasoningEffort,
@@ -36625,6 +37332,7 @@ async function createNewSessionWizard({
36625
37332
  }
36626
37333
  if (key.name === "e") {
36627
37334
  key.preventDefault();
37335
+ invalidatePendingConfirmation();
36628
37336
  step = "prompt";
36629
37337
  editor.setText(prompt);
36630
37338
  render();
@@ -36632,11 +37340,19 @@ async function createNewSessionWizard({
36632
37340
  }
36633
37341
  if (key.name === "r") {
36634
37342
  key.preventDefault();
37343
+ invalidatePendingConfirmation();
36635
37344
  step = "repositories";
36636
37345
  editor.setText("");
36637
37346
  render();
36638
37347
  return;
36639
37348
  }
37349
+ if (key.name === "b" && branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch")) {
37350
+ key.preventDefault();
37351
+ invalidatePendingConfirmation();
37352
+ step = "branches";
37353
+ render();
37354
+ return;
37355
+ }
36640
37356
  }
36641
37357
  if (step === "repositorySearch")
36642
37358
  return;
@@ -36657,8 +37373,16 @@ async function createNewSessionWizard({
36657
37373
  return;
36658
37374
  }
36659
37375
  }
37376
+ if (step === "branches" && branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch") && (key.name === "up" || key.name === "down" || key.name === "left" || key.name === "right")) {
37377
+ key.preventDefault();
37378
+ branchChoice = branchChoice === "existing" ? "new" : "existing";
37379
+ render();
37380
+ return;
37381
+ }
36660
37382
  if (!composerMounted && (key.name === "return" || key.name === "enter")) {
36661
37383
  key.preventDefault();
37384
+ if (step === "loadingBranches")
37385
+ return;
36662
37386
  submitEditor();
36663
37387
  return;
36664
37388
  }
@@ -36747,6 +37471,7 @@ async function createNewSessionWizard({
36747
37471
  let repositoryLoadState = Array.isArray(repositories) ? "ready" : "loading";
36748
37472
  let repositoryRequestGeneration = 0;
36749
37473
  let suggestionRequestGeneration = 0;
37474
+ let branchRequestGeneration = 0;
36750
37475
  let installationIds = [...new Set(loadedRepositories.map((repository) => repository.githubInstallationId))].sort((left, right) => left.localeCompare(right));
36751
37476
  let step = initialDraft ? "confirmation" : "prompt";
36752
37477
  let prompt = initialDraft?.prompt ?? "";
@@ -36758,7 +37483,9 @@ async function createNewSessionWizard({
36758
37483
  let recomputeInstruction = "";
36759
37484
  let suggestionSpinnerFrame = 0;
36760
37485
  let attachments = [...initialDraft?.attachments ?? []];
37486
+ let previousPromptEditorText = "";
36761
37487
  let pathCompletions = [];
37488
+ let selectedPathCompletions = [];
36762
37489
  let completionIndex = 0;
36763
37490
  let pathCompletionMenuOpen = false;
36764
37491
  let repositoryHeaderHeight;
@@ -36767,8 +37494,17 @@ async function createNewSessionWizard({
36767
37494
  let searchSelections;
36768
37495
  let suggestedIds = new Set;
36769
37496
  let suggestionConfidence;
37497
+ let branchSuggestions = initialDraft?.repositoryBranchOverrides?.map((override) => ({
37498
+ suggestionKind: "continueExistingBranch",
37499
+ repositoryId: override.repositoryId,
37500
+ branchName: override.branchName,
37501
+ pullRequest: null
37502
+ })) ?? [];
37503
+ let branchChoice = initialDraft?.repositoryBranchOverrides?.length ? "existing" : "new";
36770
37504
  let status = initialError ?? "";
37505
+ let attachmentFeedback;
36771
37506
  let pendingPastedImageWrites = 0;
37507
+ let pendingConfirmation;
36772
37508
  let destroyed = false;
36773
37509
  let rendererDestroyPromise;
36774
37510
  let settled = false;
@@ -36791,6 +37527,14 @@ async function createNewSessionWizard({
36791
37527
  submitEditor();
36792
37528
  },
36793
37529
  onContentChange: () => {
37530
+ if (step === "prompt") {
37531
+ selectedPathCompletions = reconcileSelectedPathCompletions({
37532
+ previousText: previousPromptEditorText,
37533
+ text: editor.plainText,
37534
+ selections: selectedPathCompletions
37535
+ });
37536
+ previousPromptEditorText = editor.plainText;
37537
+ }
36794
37538
  pathCompletionMenuOpen = false;
36795
37539
  if (step === "repositorySearch") {
36796
37540
  repositoryQuery = editor.plainText;
@@ -36826,6 +37570,35 @@ async function createNewSessionWizard({
36826
37570
  status = error93 instanceof Error ? error93.message : String(error93);
36827
37571
  }
36828
37572
  }
37573
+ async function startBranchReview() {
37574
+ const installationId = selectedInstallationId();
37575
+ if (!installationId)
37576
+ return;
37577
+ const repositoryIds = [...selectedRepositoryIds()];
37578
+ const requestGeneration = ++branchRequestGeneration;
37579
+ branchSuggestions = [];
37580
+ branchChoice = "new";
37581
+ step = "loadingBranches";
37582
+ status = "";
37583
+ render();
37584
+ try {
37585
+ const suggestions = await suggestBranches({ installationId, prompt, repositoryIds });
37586
+ if (destroyed || requestGeneration !== branchRequestGeneration || step !== "loadingBranches")
37587
+ return;
37588
+ branchSuggestions = suggestions;
37589
+ const hasExistingBranch = branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch");
37590
+ branchChoice = hasExistingBranch ? "existing" : "new";
37591
+ step = hasExistingBranch ? "branches" : "confirmation";
37592
+ } catch (error93) {
37593
+ if (destroyed || requestGeneration !== branchRequestGeneration || step !== "loadingBranches")
37594
+ return;
37595
+ branchSuggestions = [];
37596
+ branchChoice = "new";
37597
+ status = error93 instanceof Error ? error93.message : String(error93);
37598
+ step = "confirmation";
37599
+ }
37600
+ render();
37601
+ }
36829
37602
  async function openPathCompletionMenu() {
36830
37603
  if (destroyed)
36831
37604
  return;
@@ -36857,28 +37630,33 @@ async function createNewSessionWizard({
36857
37630
  }
36858
37631
  async function pasteClipboardImage() {
36859
37632
  if (!readClipboardImage || !savePastedImage) {
36860
- status = "Clipboard image paste is unavailable.";
37633
+ attachmentFeedback = { kind: "failure", message: "Clipboard image paste is unavailable." };
36861
37634
  render();
36862
37635
  return;
36863
37636
  }
36864
37637
  pendingPastedImageWrites += 1;
36865
- status = "Saving clipboard image\u2026";
37638
+ attachmentFeedback = { kind: "progress", message: "Saving clipboard image\u2026" };
36866
37639
  render();
36867
37640
  try {
36868
37641
  const image = await readClipboardImage();
36869
37642
  const path = await savePastedImage(image);
36870
37643
  if (!destroyed) {
36871
37644
  editor.insertText(path);
36872
- status = "Clipboard image added; continue to attach it.";
37645
+ const selectedCompletion = createSelectedPathCompletion({ attachmentPath: path, token: path, end: editor.cursorOffset });
37646
+ selectedPathCompletions = [...selectedPathCompletions, selectedCompletion];
37647
+ previousPromptEditorText = editor.plainText;
37648
+ attachmentFeedback = { kind: "success", message: "Clipboard image ready to attach." };
36873
37649
  }
36874
37650
  } catch (error93) {
36875
- status = error93 instanceof Error ? error93.message : String(error93);
37651
+ attachmentFeedback = { kind: "failure", message: error93 instanceof Error ? error93.message : String(error93) };
36876
37652
  } finally {
36877
37653
  pendingPastedImageWrites -= 1;
36878
37654
  }
36879
37655
  render();
36880
37656
  }
36881
37657
  async function confirm() {
37658
+ if (destroyed || settled || step !== "confirmation" || pendingConfirmation)
37659
+ return;
36882
37660
  const installationId = selectedInstallationId();
36883
37661
  if (!installationId) {
36884
37662
  status = "GitHub installation selection is required.";
@@ -36890,22 +37668,55 @@ async function createNewSessionWizard({
36890
37668
  fail(new Error("Selected repositories must belong to the chosen GitHub installation."));
36891
37669
  return;
36892
37670
  }
37671
+ const repositoryBranchOverrides = branchChoice === "existing" ? branchSuggestions.flatMap((suggestion) => suggestion.suggestionKind === "createSessionBranch" ? [] : [{ repositoryId: suggestion.repositoryId, branchName: suggestion.branchName }]) : [];
37672
+ const confirmation = Symbol("confirmation");
37673
+ const confirmedAttachments = [...attachments];
37674
+ const confirmedPrompt = prompt;
37675
+ const confirmedModel = model;
37676
+ const confirmedReasoningEffort = reasoningEffort;
37677
+ const confirmedSelectedPaths = selectedAttachmentPaths({ text: confirmedPrompt, selections: selectedPathCompletions });
37678
+ pendingConfirmation = confirmation;
37679
+ const ownsConfirmation = () => !destroyed && !settled && step === "confirmation" && pendingConfirmation === confirmation;
36893
37680
  if (attachPromptPaths) {
36894
- status = "Attaching referenced files\u2026";
36895
- render();
37681
+ attachmentFeedback = undefined;
36896
37682
  try {
36897
37683
  const promptAttachments = await attachPromptPaths({
36898
- text: prompt,
36899
- excludedPaths: attachments.flatMap((attachment) => attachment.sourcePath ? [attachment.sourcePath] : [])
37684
+ text: confirmedPrompt,
37685
+ excludedPaths: confirmedAttachments.flatMap((attachment) => attachment.sourcePath ? [attachment.sourcePath] : []),
37686
+ selectedPaths: confirmedSelectedPaths,
37687
+ onUploadStart: ({ filenames }) => {
37688
+ if (!ownsConfirmation())
37689
+ return;
37690
+ attachmentFeedback = { kind: "progress", message: attachmentUploadMessage(filenames) };
37691
+ render();
37692
+ }
36900
37693
  });
36901
- attachments = [...attachments, ...promptAttachments];
37694
+ if (!ownsConfirmation())
37695
+ return;
37696
+ confirmedAttachments.push(...promptAttachments);
37697
+ attachments = confirmedAttachments;
37698
+ selectedPathCompletions = [];
37699
+ attachmentFeedback = undefined;
36902
37700
  } catch (error93) {
36903
- status = error93 instanceof Error ? error93.message : String(error93);
37701
+ if (!ownsConfirmation())
37702
+ return;
37703
+ pendingConfirmation = undefined;
37704
+ attachmentFeedback = { kind: "failure", message: error93 instanceof Error ? error93.message : String(error93) };
36904
37705
  render();
36905
37706
  return;
36906
37707
  }
36907
37708
  }
36908
- finish({ prompt, githubInstallationId: installationId, repositories: selected, attachments, model, reasoningEffort });
37709
+ if (!ownsConfirmation())
37710
+ return;
37711
+ finish({
37712
+ prompt: confirmedPrompt,
37713
+ githubInstallationId: installationId,
37714
+ repositories: selected,
37715
+ ...repositoryBranchOverrides.length > 0 ? { repositoryBranchOverrides } : {},
37716
+ attachments: confirmedAttachments,
37717
+ model: confirmedModel,
37718
+ reasoningEffort: confirmedReasoningEffort
37719
+ });
36909
37720
  }
36910
37721
  async function submitEditor() {
36911
37722
  const value = editor.plainText.trim();
@@ -36939,8 +37750,12 @@ async function createNewSessionWizard({
36939
37750
  return;
36940
37751
  }
36941
37752
  if (step === "repositories") {
36942
- step = "confirmation";
36943
37753
  editor.setText("");
37754
+ startBranchReview();
37755
+ return;
37756
+ }
37757
+ if (step === "branches") {
37758
+ step = "confirmation";
36944
37759
  render();
36945
37760
  return;
36946
37761
  }
@@ -37004,6 +37819,7 @@ async function createNewSessionWizard({
37004
37819
  destroy() {
37005
37820
  if (rendererDestroyed2)
37006
37821
  return;
37822
+ invalidatePendingConfirmation();
37007
37823
  destroyed = true;
37008
37824
  if (inputHandler)
37009
37825
  renderer.removeInputHandler(inputHandler);
@@ -37040,12 +37856,13 @@ async function createNewSessionWizard({
37040
37856
  }
37041
37857
  }
37042
37858
  function stepHeader(title) {
37043
- return bold3(fg5(PALETTE.remyAccent)(title));
37859
+ return bold3(fg6(PALETTE.remyAccent)(title));
37044
37860
  }
37045
37861
  var WIZARD_STEPS = [
37046
37862
  { key: "describe", label: "Describe" },
37047
37863
  { key: "installation", label: "Installation" },
37048
37864
  { key: "repositories", label: "Repositories" },
37865
+ { key: "branches", label: "Branches" },
37049
37866
  { key: "confirm", label: "Confirm" }
37050
37867
  ];
37051
37868
  function wizardStepKey(step) {
@@ -37053,52 +37870,41 @@ function wizardStepKey(step) {
37053
37870
  return "describe";
37054
37871
  if (step === "installation")
37055
37872
  return "installation";
37873
+ if (step === "loadingBranches" || step === "branches")
37874
+ return "branches";
37056
37875
  if (step === "confirmation")
37057
37876
  return "confirm";
37058
37877
  return "repositories";
37059
37878
  }
37060
- function renderStepIndicator({ step, hasInstallationChoice }) {
37061
- const steps = WIZARD_STEPS.filter((entry) => entry.key !== "installation" || hasInstallationChoice);
37879
+ function renderStepIndicator({ step, hasInstallationChoice, hasBranchChoice = false }) {
37880
+ const steps = WIZARD_STEPS.filter((entry) => (entry.key !== "installation" || hasInstallationChoice) && (entry.key !== "branches" || hasBranchChoice));
37062
37881
  const currentKey = wizardStepKey(step);
37063
37882
  const currentIndex = steps.findIndex((entry) => entry.key === currentKey);
37064
37883
  const position = currentIndex >= 0 ? currentIndex + 1 : steps.length;
37065
37884
  const crumbs = [];
37066
37885
  steps.forEach((entry, index) => {
37067
37886
  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));
37887
+ crumbs.push(fg6(PALETTE.dimText)(" \u203A "));
37888
+ crumbs.push(entry.key === currentKey ? bold3(fg6(PALETTE.bodyText)(entry.label)) : fg6(PALETTE.dimText)(entry.label));
37070
37889
  });
37071
- return new StyledText4([
37072
- bold3(fg5(PALETTE.remyAccent)(`Step ${position} of ${steps.length}`)),
37073
- fg5(PALETTE.dimText)(" "),
37890
+ return new StyledText5([
37891
+ bold3(fg6(PALETTE.remyAccent)(`Step ${position} of ${steps.length}`)),
37892
+ fg6(PALETTE.dimText)(" "),
37074
37893
  ...crumbs
37075
37894
  ]);
37076
37895
  }
37077
37896
  function renderSelectableRow({ label, isCursor }) {
37078
37897
  const line = `${isCursor ? "> " : " "}${label}`;
37079
- const styled = fg5(PALETTE.bodyText)(line);
37898
+ const styled = fg6(PALETTE.bodyText)(line);
37080
37899
  return [isCursor ? bg3(PALETTE.selectionBg)(styled) : styled];
37081
37900
  }
37082
37901
  function renderRepositoryRow({ repository, isCursor, isChecked, source }) {
37083
37902
  const marker = isChecked ? "[x]" : "[ ]";
37084
37903
  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;
37904
+ const styled = isChecked ? bold3(fg6(PALETTE.humanAccent)(line)) : fg6(PALETTE.dimText)(line);
37905
+ const tag = source === "suggested" && isChecked ? dim4(fg6(PALETTE.tool)(" \u25C6 suggested by Remy")) : undefined;
37087
37906
  return [isCursor ? bg3(PALETTE.selectionBg)(styled) : styled, ...tag ? [tag] : []];
37088
37907
  }
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
37908
  function cycle({ values, value, direction }) {
37103
37909
  const currentIndex = values.indexOf(value);
37104
37910
  const nextIndex = (currentIndex + direction + values.length) % values.length;
@@ -37114,7 +37920,7 @@ async function createDefaultRenderer2() {
37114
37920
  }
37115
37921
 
37116
37922
  // 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";
37923
+ 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
37924
  var composerSlashCommands = [
37119
37925
  { value: "/sessions", description: "Back to the session list" },
37120
37926
  { value: "/complete", description: "Finish this session" },
@@ -37195,6 +38001,7 @@ async function createSessionTui({
37195
38001
  }
37196
38002
  promptHistoryIndex = direction === "up" ? Math.max(0, promptHistoryIndex - 1) : Math.min(promptHistory.length, promptHistoryIndex + 1);
37197
38003
  const text = promptHistoryIndex === promptHistory.length ? promptHistoryDraft : promptHistory[promptHistoryIndex];
38004
+ selectedPathCompletions = [];
37198
38005
  historyReplacement = text;
37199
38006
  composer.setText(text);
37200
38007
  composer.cursorOffset = text.length;
@@ -37217,10 +38024,10 @@ async function createSessionTui({
37217
38024
  syncTerminalSessionControls();
37218
38025
  syncWorkingIndicator();
37219
38026
  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([]);
38027
+ const assistantPreview = latestState.previews.assistantText ? new StyledText6([dim5(fg7(PALETTE.dimText)(`Live assistant preview
38028
+ `)), fg7(PALETTE.remyAccent)(latestState.previews.assistantText)]) : new StyledText6([]);
38029
+ const reasoningPreview = latestState.previews.reasoningText ? new StyledText6([dim5(fg7(PALETTE.dimText)(`Live reasoning preview
38030
+ `)), dim5(fg7(PALETTE.dimText)(latestState.previews.reasoningText))]) : new StyledText6([]);
37224
38031
  const working = isRemyWorking(latestState);
37225
38032
  const startedAt = workingTurnStartedAt(latestState);
37226
38033
  const elapsedMs = startedAt === undefined ? 0 : Math.max(0, now3() - Date.parse(startedAt));
@@ -37229,7 +38036,7 @@ async function createSessionTui({
37229
38036
  frame: workingSpinnerFrames[spinnerFrameIndex],
37230
38037
  elapsedMs,
37231
38038
  mode: stopState.kind === "stopping" ? "stopping" : "working"
37232
- }) : new StyledText5([]);
38039
+ }) : new StyledText6([]);
37233
38040
  const liveContent = [assistantPreview, reasoningPreview, liveIndicator].filter((part) => part.chunks.length > 0);
37234
38041
  liveTranscript.content = liveContent.length === 0 ? "" : joinStyled([stringToStyledText5(`
37235
38042
 
@@ -37245,6 +38052,7 @@ async function createSessionTui({
37245
38052
  const commandMenu = renderSlashCommandMenu({ commands: slashCompletions, selectedIndex: slashCompletionIndex });
37246
38053
  const composerStatus = [
37247
38054
  helpVisible ? renderHelpPanel() : undefined,
38055
+ attachmentFeedback ? renderAttachmentFeedback(attachmentFeedback) : undefined,
37248
38056
  composerFeedback ? stringToStyledText5(composerFeedback) : undefined,
37249
38057
  submissionStatus,
37250
38058
  commandMenu,
@@ -37317,10 +38125,17 @@ async function createSessionTui({
37317
38125
  }
37318
38126
  slashCompletions = composerSlashCommands.filter((command) => command.value.startsWith(value) && command.value !== value && (!["/complete", "/cancel"].includes(command.value) || latestState.aggregateStatus === "open"));
37319
38127
  slashCompletionIndex = 0;
38128
+ }, invalidateAttachmentPreparation = function() {
38129
+ attachmentPreparationOwner = undefined;
38130
+ }, ownsAttachmentPreparation = function(owner) {
38131
+ return attachmentPreparationOwner === owner && !destroyed && !settled;
38132
+ }, viewIsActive = function() {
38133
+ return !destroyed && !settled;
37320
38134
  }, finish = function(result, error93) {
37321
38135
  if (settled)
37322
38136
  return;
37323
38137
  settled = true;
38138
+ invalidateAttachmentPreparation();
37324
38139
  if (error93)
37325
38140
  rejectAction(error93);
37326
38141
  else
@@ -37344,17 +38159,21 @@ async function createSessionTui({
37344
38159
  let settled = false;
37345
38160
  let activityExpanded = false;
37346
38161
  let composerDraft = initialComposer ?? { text: "", attachments: [] };
38162
+ let previousComposerText = composerDraft.text;
37347
38163
  let pathCompletions = [];
38164
+ let selectedPathCompletions = [];
37348
38165
  let completionIndex = 0;
37349
38166
  let pathCompletionMenuOpen = false;
37350
38167
  let slashCompletions = [];
37351
38168
  let slashCompletionIndex = 0;
37352
38169
  let composerFeedback = initialFeedback;
38170
+ let attachmentFeedback;
37353
38171
  const localPromptHistory = [];
37354
38172
  let promptHistoryIndex;
37355
38173
  let promptHistoryDraft = "";
37356
38174
  let historyReplacement;
37357
38175
  let pendingPastedImageWrites = 0;
38176
+ let attachmentPreparationOwner;
37358
38177
  let admittedSubmissions = [];
37359
38178
  let stopState = { kind: "idle" };
37360
38179
  let lifecycleRequestState = { kind: "idle" };
@@ -37393,6 +38212,12 @@ async function createSessionTui({
37393
38212
  submitComposer();
37394
38213
  },
37395
38214
  onContentChange: () => {
38215
+ selectedPathCompletions = reconcileSelectedPathCompletions({
38216
+ previousText: previousComposerText,
38217
+ text: composer.plainText,
38218
+ selections: selectedPathCompletions
38219
+ });
38220
+ previousComposerText = composer.plainText;
37396
38221
  if (historyReplacement !== composer.plainText)
37397
38222
  promptHistoryIndex = undefined;
37398
38223
  historyReplacement = undefined;
@@ -37473,12 +38298,12 @@ async function createSessionTui({
37473
38298
  };
37474
38299
  async function pasteClipboardImage() {
37475
38300
  if (!readClipboardImage || !savePastedImage) {
37476
- composerFeedback = "Clipboard image paste is unavailable.";
38301
+ attachmentFeedback = { kind: "failure", message: "Clipboard image paste is unavailable." };
37477
38302
  render();
37478
38303
  return;
37479
38304
  }
37480
38305
  pendingPastedImageWrites += 1;
37481
- composerFeedback = "Saving clipboard image\u2026";
38306
+ attachmentFeedback = { kind: "progress", message: "Saving clipboard image\u2026" };
37482
38307
  render();
37483
38308
  try {
37484
38309
  const image = await readClipboardImage();
@@ -37486,10 +38311,13 @@ async function createSessionTui({
37486
38311
  if (destroyed)
37487
38312
  return;
37488
38313
  composer.insertText(path);
37489
- composerFeedback = "Clipboard image added; send message to attach it.";
38314
+ const selectedCompletion = createSelectedPathCompletion({ attachmentPath: path, token: path, end: composer.cursorOffset });
38315
+ selectedPathCompletions = [...selectedPathCompletions, selectedCompletion];
38316
+ previousComposerText = composer.plainText;
38317
+ attachmentFeedback = { kind: "success", message: "Clipboard image ready to attach." };
37490
38318
  } catch (error93) {
37491
38319
  if (!destroyed)
37492
- composerFeedback = error93 instanceof Error ? error93.message : String(error93);
38320
+ attachmentFeedback = { kind: "failure", message: error93 instanceof Error ? error93.message : String(error93) };
37493
38321
  } finally {
37494
38322
  pendingPastedImageWrites -= 1;
37495
38323
  if (!destroyed)
@@ -37549,6 +38377,15 @@ async function createSessionTui({
37549
38377
  const completedText = replaceActivePathToken({ text: composer.plainText, replacement: completion.replacement });
37550
38378
  composer.setText(completedText);
37551
38379
  composer.cursorOffset = completedText.length;
38380
+ if (completion.attachmentPath) {
38381
+ const selectedCompletion = createSelectedPathCompletion({
38382
+ attachmentPath: completion.attachmentPath,
38383
+ token: completion.replacement,
38384
+ end: completedText.length
38385
+ });
38386
+ selectedPathCompletions = [...selectedPathCompletions, selectedCompletion];
38387
+ }
38388
+ previousComposerText = completedText;
37552
38389
  }
37553
38390
  pathCompletions = [];
37554
38391
  pathCompletionMenuOpen = false;
@@ -37579,8 +38416,10 @@ async function createSessionTui({
37579
38416
  render();
37580
38417
  }
37581
38418
  async function submitComposer() {
38419
+ if (attachmentPreparationOwner)
38420
+ return;
37582
38421
  if (pendingPastedImageWrites > 0) {
37583
- composerFeedback = "Saving pasted image\u2026";
38422
+ attachmentFeedback = { kind: "progress", message: "Saving pasted image\u2026" };
37584
38423
  render();
37585
38424
  return;
37586
38425
  }
@@ -37588,33 +38427,61 @@ async function createSessionTui({
37588
38427
  const value = composerDraft.text;
37589
38428
  if (value.trim() === "")
37590
38429
  return;
37591
- if (await handleComposerCommand(value))
38430
+ const preparationOwner = Symbol("attachment preparation");
38431
+ attachmentPreparationOwner = preparationOwner;
38432
+ if (await handleComposerCommand(value)) {
38433
+ if (attachmentPreparationOwner === preparationOwner)
38434
+ invalidateAttachmentPreparation();
38435
+ return;
38436
+ }
38437
+ if (!ownsAttachmentPreparation(preparationOwner))
37592
38438
  return;
37593
38439
  if (lifecycleRequestState.kind === "pending") {
37594
38440
  composerFeedback = `${lifecycleOperationPresentParticiple(lifecycleRequestState.operation)} the session \u2014 messages are paused.`;
37595
38441
  render();
38442
+ invalidateAttachmentPreparation();
37596
38443
  return;
37597
38444
  }
37598
38445
  if (latestState.aggregateStatus !== "open") {
37599
38446
  composerFeedback = "This session is already terminal. Run /sessions to return to the session list.";
37600
38447
  render();
38448
+ invalidateAttachmentPreparation();
37601
38449
  return;
37602
38450
  }
37603
38451
  if (attachPromptPaths) {
37604
- composerFeedback = "Attaching referenced files\u2026";
37605
- render();
38452
+ let handedOffToAdmission = false;
37606
38453
  try {
37607
38454
  const attachments = await attachPromptPaths({
37608
38455
  text: composerDraft.text,
37609
- excludedPaths: composerDraft.attachments.flatMap((attachment) => attachment.sourcePath ? [attachment.sourcePath] : [])
38456
+ excludedPaths: composerDraft.attachments.flatMap((attachment) => attachment.sourcePath ? [attachment.sourcePath] : []),
38457
+ selectedPaths: selectedAttachmentPaths({ text: composerDraft.text, selections: selectedPathCompletions }),
38458
+ onUploadStart: ({ filenames }) => {
38459
+ if (!ownsAttachmentPreparation(preparationOwner))
38460
+ return;
38461
+ attachmentFeedback = { kind: "progress", message: attachmentUploadMessage(filenames) };
38462
+ render();
38463
+ }
37610
38464
  });
38465
+ if (!ownsAttachmentPreparation(preparationOwner))
38466
+ return;
37611
38467
  composerDraft = { ...composerDraft, attachments: [...composerDraft.attachments, ...attachments] };
38468
+ selectedPathCompletions = [];
38469
+ attachmentFeedback = undefined;
38470
+ handedOffToAdmission = true;
37612
38471
  } catch (error93) {
37613
- composerFeedback = error93 instanceof Error ? error93.message : String(error93);
38472
+ if (!ownsAttachmentPreparation(preparationOwner))
38473
+ return;
38474
+ attachmentFeedback = { kind: "failure", message: error93 instanceof Error ? error93.message : String(error93) };
37614
38475
  render();
37615
38476
  return;
38477
+ } finally {
38478
+ if (!handedOffToAdmission && attachmentPreparationOwner === preparationOwner)
38479
+ invalidateAttachmentPreparation();
37616
38480
  }
37617
38481
  }
38482
+ if (!ownsAttachmentPreparation(preparationOwner))
38483
+ return;
38484
+ invalidateAttachmentPreparation();
37618
38485
  const submission = {
37619
38486
  id: crypto.randomUUID(),
37620
38487
  idempotencyKey: crypto.randomUUID(),
@@ -37651,13 +38518,13 @@ async function createSessionTui({
37651
38518
  if (command === "/complete") {
37652
38519
  composer.setText("");
37653
38520
  composerDraft = { ...composerDraft, text: "" };
37654
- await handleLifecycleRequest("complete");
38521
+ handleLifecycleRequest("complete");
37655
38522
  return true;
37656
38523
  }
37657
38524
  if (command === "/cancel") {
37658
38525
  composer.setText("");
37659
38526
  composerDraft = { ...composerDraft, text: "" };
37660
- await handleLifecycleRequest("cancel");
38527
+ handleLifecycleRequest("cancel");
37661
38528
  return true;
37662
38529
  }
37663
38530
  if (command === "/logout") {
@@ -37685,16 +38552,22 @@ async function createSessionTui({
37685
38552
  attachments: submission.attachments,
37686
38553
  idempotencyKey: submission.idempotencyKey
37687
38554
  });
38555
+ if (!viewIsActive())
38556
+ return;
37688
38557
  localPromptHistory.push(submission.text);
37689
38558
  const messageId = latestState.activeMessageId;
37690
38559
  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
38560
  composerFeedback = undefined;
37692
38561
  } catch (error93) {
38562
+ if (!viewIsActive())
38563
+ return;
37693
38564
  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
38565
  }
37695
38566
  render();
37696
38567
  }
37697
38568
  async function retryOldestFailedSubmission() {
38569
+ if (!viewIsActive())
38570
+ return;
37698
38571
  const failed = admittedSubmissions.find((submission) => submission.status === "failed");
37699
38572
  if (!failed)
37700
38573
  return;
@@ -37810,6 +38683,7 @@ async function createSessionTui({
37810
38683
  if (destroyed)
37811
38684
  return;
37812
38685
  destroyed = true;
38686
+ invalidateAttachmentPreparation();
37813
38687
  if (workingSpinnerTimer) {
37814
38688
  clearInterval(workingSpinnerTimer);
37815
38689
  workingSpinnerTimer = undefined;
@@ -37851,9 +38725,9 @@ function renderSlashCommandMenu({ commands, selectedIndex }) {
37851
38725
  const rows = commands.map((command, index) => {
37852
38726
  const selected = index === selectedIndex;
37853
38727
  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))
38728
+ return new StyledText6([
38729
+ rowStyle(fg7(PALETTE.remyAccent)(`${selected ? "\u203A " : " "}${command.value.padEnd(12)}`)),
38730
+ rowStyle(fg7(PALETTE.dimText)(command.description))
37857
38731
  ]);
37858
38732
  });
37859
38733
  return joinStyled([joinStyled(rows, `
@@ -37874,15 +38748,15 @@ var helpEntries = [
37874
38748
  ];
37875
38749
  function renderHelpPanel() {
37876
38750
  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"))])];
38751
+ const rows = [new StyledText6([dim5(fg7(PALETTE.dimText)("What you can do here"))])];
37878
38752
  let lastGroup;
37879
38753
  for (const entry of helpEntries) {
37880
38754
  if (lastGroup !== undefined && entry.group !== lastGroup)
37881
- rows.push(new StyledText5([fg6(PALETTE.dimText)(" ")]));
38755
+ rows.push(new StyledText6([fg7(PALETTE.dimText)(" ")]));
37882
38756
  lastGroup = entry.group;
37883
- rows.push(new StyledText5([
37884
- fg6(PALETTE.remyAccent)(` ${entry.token.padEnd(width)}`),
37885
- dim4(fg6(PALETTE.dimText)(entry.description))
38757
+ rows.push(new StyledText6([
38758
+ fg7(PALETTE.remyAccent)(` ${entry.token.padEnd(width)}`),
38759
+ dim5(fg7(PALETTE.dimText)(entry.description))
37886
38760
  ]));
37887
38761
  }
37888
38762
  return joinStyled(rows, `
@@ -37896,23 +38770,22 @@ function renderStatusBand({ state, working, elapsedMs, stopState, lifecycleReque
37896
38770
  const pendingOperation = lifecycleRequestState?.kind === "pending" ? lifecycleRequestState.operation : undefined;
37897
38771
  const presentation = pendingOperation ? { label: lifecycleOperationPresentParticiple(pendingOperation), color: PALETTE.approvalQuestion } : stopping ? { label: "Stopping", color: PALETTE.approvalQuestion } : sessionViewStatePresentation({ aggregateStatus: state.aggregateStatus, isWorking: working });
37898
38772
  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);
38773
+ const connection = state.connectionStatus === "connected" ? new StyledText6([fg7(PALETTE.statusCompleted)("\u25CF "), dim5(fg7(PALETTE.dimText)("connected"))]) : new StyledText6([fg7(PALETTE.approvalQuestion)("\u25CF reconnecting\u2026")]);
38774
+ const sessionLabel = state.sessionNumber !== undefined ? `#${state.sessionNumber}` : state.sessionId;
37902
38775
  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
- ]);
38776
+ const contextStatus = renderContextStatus(state.context.snapshot);
38777
+ const headerContext = [
38778
+ ...repositoryLabel ? [[dim5(fg7(PALETTE.dimText)(repositoryLabel))]] : [],
38779
+ ...pullRequest ? [[bold4(fg7(statusColor(pullRequest.status))(`PR#${pullRequest.number}`))]] : [],
38780
+ [bold4(fg7(presentation.color)(`${presentation.label}${elapsed}`))],
38781
+ connection.chunks,
38782
+ ...contextStatus ? [[dim5(fg7(PALETTE.dimText)(contextStatus))]] : []
38783
+ ];
38784
+ return joinStyled([
38785
+ [bold4(fg7(PALETTE.bodyText)(`${sessionLabel} ${state.title ?? "Untitled session"}`))],
38786
+ joinStyled(headerContext, " \xB7 ")
38787
+ ], `
38788
+ `);
37916
38789
  }
37917
38790
  function renderContextStatus(snapshot) {
37918
38791
  if (snapshot.status === "unknown")
@@ -37923,33 +38796,39 @@ function renderContextStatus(snapshot) {
37923
38796
  function renderTerminalSessionBand({ aggregateStatus }) {
37924
38797
  const status = sessionStatusLabel(aggregateStatus).toLowerCase();
37925
38798
  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. "))
38799
+ new StyledText6([
38800
+ bg4(PALETTE.selectionBg)(bold4(fg7(PALETTE.systemAccent)(` Session ${status} `))),
38801
+ bg4(PALETTE.selectionBg)(fg7(PALETTE.bodyText)("\u2014 this conversation is closed. "))
37929
38802
  ]),
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 ")))
38803
+ new StyledText6([
38804
+ bg4(PALETTE.selectionBg)(fg7(PALETTE.remyAccent)(" esc: ")),
38805
+ bg4(PALETTE.selectionBg)(dim5(fg7(PALETTE.dimText)("return to dashboard \xB7 "))),
38806
+ bg4(PALETTE.selectionBg)(fg7(PALETTE.remyAccent)("n: ")),
38807
+ bg4(PALETTE.selectionBg)(dim5(fg7(PALETTE.dimText)("new session ")))
37935
38808
  ])
37936
38809
  ], `
37937
38810
  `);
37938
38811
  }
37939
38812
  function renderActionBar({ state, working, stopState, lifecycleRequestState }) {
37940
38813
  if (lifecycleRequestState.kind === "pending")
37941
- return new StyledText5([dim4(fg6(PALETTE.dimText)(`${lifecycleOperationPresentParticiple(lifecycleRequestState.operation).toLowerCase()} the session\u2026`))]);
38814
+ return new StyledText6([dim5(fg7(PALETTE.dimText)(`${lifecycleOperationPresentParticiple(lifecycleRequestState.operation).toLowerCase()} the session\u2026`))]);
37942
38815
  if (state.aggregateStatus !== "open")
37943
- return new StyledText5([]);
38816
+ return new StyledText6([]);
37944
38817
  const stopToken = working && stopState.kind === "failed" ? ["esc retry stop"] : [];
37945
38818
  const idleGroup = lifecycleRequestState.kind === "failed" ? `/${lifecycleRequestState.operation} retries \xB7 /new \xB7 /sessions` : "/new \xB7 /sessions \xB7 /complete \xB7 /cancel";
37946
38819
  const tokens = working ? ["/ commands", ...stopToken, "ctrl+o activity"] : ["/ commands", idleGroup, "ctrl+o activity"];
37947
- return new StyledText5([dim4(fg6(PALETTE.dimText)(tokens.join(" \xB7 ")))]);
38820
+ return new StyledText6([dim5(fg7(PALETTE.dimText)(tokens.join(" \xB7 ")))]);
37948
38821
  }
37949
38822
  function isRemyWorking(state) {
37950
- if (state.aggregateStatus !== "open" || state.connectionStatus !== "connected")
38823
+ if (state.aggregateStatus !== "open" || state.connectionStatus !== "connected") {
37951
38824
  return false;
37952
- return hasRunningTurn(state) || state.activeMessageId !== undefined && state.messageTurns[state.activeMessageId]?.outcome === undefined;
38825
+ }
38826
+ if (state.activeMessageId !== undefined && state.messageTurns[state.activeMessageId]?.outcome === undefined)
38827
+ return true;
38828
+ return isWorkingAgentStatus(state.agentStatus) && hasRunningTurn(state);
38829
+ }
38830
+ function isWorkingAgentStatus(agentStatus) {
38831
+ return agentStatus === undefined || agentStatus === "pending" || agentStatus === "queued" || agentStatus === "running";
37953
38832
  }
37954
38833
  function workingTurnStartedAt(state) {
37955
38834
  const activeTurn = state.activeMessageId === undefined ? undefined : state.messageTurns[state.activeMessageId];
@@ -37990,58 +38869,64 @@ function renderTimeline({ state, activityExpanded }) {
37990
38869
  }
37991
38870
  function renderTimelineItem(item, activityExpanded) {
37992
38871
  if (item.kind === "message") {
37993
- const header = new StyledText5([
38872
+ const header = new StyledText6([
37994
38873
  renderTimestampChunk(item.occurredAt),
37995
38874
  renderRoleLabel({ label: item.author.label, role: item.author.role }),
37996
- ...item.author.source ? [dim4(fg6(PALETTE.dimText)(` (${item.author.source})`))] : []
38875
+ ...item.author.source ? [dim5(fg7(PALETTE.dimText)(` (${item.author.source})`))] : []
37997
38876
  ]);
37998
- return joinStyled([header, renderMessageBody({ text: item.text, role: item.author.role })], `
38877
+ const attachments = renderAttachmentSummary(item.attachments);
38878
+ return joinStyled([
38879
+ header,
38880
+ renderMessageBody({ text: item.text, role: item.author.role }),
38881
+ ...attachments.chunks.length > 0 ? [attachments] : []
38882
+ ], `
37999
38883
  `);
38000
38884
  }
38001
38885
  if (item.kind === "plan")
38002
38886
  return renderPlan({ item, expanded: activityExpanded });
38003
38887
  if (item.artifactKind === "tela_page") {
38004
- return new StyledText5([
38888
+ return new StyledText6([
38005
38889
  renderTimestampChunk(item.occurredAt),
38006
- bold4(fg6(PALETTE.tool)("Tela Page: ")),
38007
- fg6(PALETTE.bodyText)(item.title),
38008
- dim4(fg6(PALETTE.dimText)(` \xB7 ${item.url}`))
38890
+ bold4(fg7(PALETTE.tool)("Tela Page: ")),
38891
+ fg7(PALETTE.bodyText)(item.title),
38892
+ dim5(fg7(PALETTE.dimText)(` \xB7 ${item.url}`))
38009
38893
  ]);
38010
38894
  }
38011
- return new StyledText5([
38895
+ return new StyledText6([
38012
38896
  renderTimestampChunk(item.occurredAt),
38013
- bold4(fg6(PALETTE.tool)("Artifact: ")),
38014
- fg6(PALETTE.bodyText)(item.title)
38897
+ bold4(fg7(PALETTE.tool)("Artifact: ")),
38898
+ fg7(PALETTE.bodyText)(item.title),
38899
+ ...item.previewUrl ? [dim5(fg7(PALETTE.dimText)(` (${item.previewUrl})`))] : []
38015
38900
  ]);
38016
38901
  }
38017
38902
  var collapsedPlanItemLimit = 5;
38018
38903
  function renderPlan({ item, expanded }) {
38019
38904
  if (item.items.length === 0)
38020
- return new StyledText5([]);
38905
+ return new StyledText6([]);
38021
38906
  const completed = item.items.filter((workItem) => workItem.status === "completed").length;
38022
38907
  const visibleItems = expanded ? item.items : collapsedPlanItems(item.items);
38023
38908
  const rows = visibleItems.map((workItem) => {
38024
38909
  if (workItem.status === "in_progress") {
38025
- return new StyledText5([
38026
- fg6(PALETTE.remyAccent)(" \u25B8 "),
38027
- bold4(fg6(PALETTE.bodyText)(workItem.title))
38910
+ return new StyledText6([
38911
+ fg7(PALETTE.remyAccent)(" \u25B8 "),
38912
+ bold4(fg7(PALETTE.bodyText)(workItem.title))
38028
38913
  ]);
38029
38914
  }
38030
38915
  const glyph = workItem.status === "completed" ? "\u2713" : "\u25CB";
38031
- return new StyledText5([
38032
- fg6(PALETTE.dimText)(` ${glyph} `),
38033
- dim4(fg6(PALETTE.dimText)(workItem.title))
38916
+ return new StyledText6([
38917
+ fg7(PALETTE.dimText)(` ${glyph} `),
38918
+ dim5(fg7(PALETTE.dimText)(workItem.title))
38034
38919
  ]);
38035
38920
  });
38036
38921
  const hidden = item.items.length - visibleItems.length;
38037
38922
  return joinStyled([
38038
- new StyledText5([
38923
+ new StyledText6([
38039
38924
  renderTimestampChunk(item.occurredAt),
38040
- bold4(fg6(PALETTE.remyAccent)("Plan")),
38041
- dim4(fg6(PALETTE.dimText)(` \xB7 ${completed}/${item.items.length} done`))
38925
+ bold4(fg7(PALETTE.remyAccent)("Plan")),
38926
+ dim5(fg7(PALETTE.dimText)(` \xB7 ${completed}/${item.items.length} done`))
38042
38927
  ]),
38043
38928
  ...rows,
38044
- ...hidden > 0 ? [new StyledText5([dim4(fg6(PALETTE.dimText)(` \u2026 ${hidden} more task${hidden === 1 ? "" : "s"} \xB7 Ctrl+O details`))])] : []
38929
+ ...hidden > 0 ? [new StyledText6([dim5(fg7(PALETTE.dimText)(` \u2026 ${hidden} more task${hidden === 1 ? "" : "s"} \xB7 Ctrl+O details`))])] : []
38045
38930
  ], `
38046
38931
  `);
38047
38932
  }
@@ -38088,7 +38973,7 @@ function canonicalValuesEqual(left, right) {
38088
38973
  }
38089
38974
  function renderActivityGroup({ items, activityExpanded }) {
38090
38975
  if (items.length === 0)
38091
- return new StyledText5([]);
38976
+ return new StyledText6([]);
38092
38977
  const summary = renderActivitySummary({ items, includeTimestamp: activityExpanded });
38093
38978
  const lastInFlightIndex = items.length - 1;
38094
38979
  if (!activityExpanded) {
@@ -38097,9 +38982,9 @@ function renderActivityGroup({ items, activityExpanded }) {
38097
38982
  item,
38098
38983
  inFlight: item.card.kind === "tool" && tailStart + index === lastInFlightIndex
38099
38984
  }));
38100
- const elision = tailStart > 0 ? new StyledText5([dim4(fg6(PALETTE.dimText)(` \u2026 ${tailStart} earlier step${tailStart === 1 ? "" : "s"}`))]) : undefined;
38985
+ const elision = tailStart > 0 ? new StyledText6([dim5(fg7(PALETTE.dimText)(` \u2026 ${tailStart} earlier step${tailStart === 1 ? "" : "s"}`))]) : undefined;
38101
38986
  return joinStyled([
38102
- new StyledText5([renderTimestampChunk(items[0].occurredAt), dim4(fg6(PALETTE.dimText)("Worked"))]),
38987
+ new StyledText6([renderTimestampChunk(items[0].occurredAt), dim5(fg7(PALETTE.dimText)("Worked"))]),
38103
38988
  ...elision ? [elision] : [],
38104
38989
  ...tail,
38105
38990
  summary
@@ -38118,9 +39003,9 @@ function renderExpandedActivityHierarchy({ items, lastInFlightIndex }) {
38118
39003
  const attribution = item.card.attribution;
38119
39004
  if (attribution?.status === "resolved") {
38120
39005
  if (activeOwnerKey !== attribution.ownerKey) {
38121
- rendered.push(new StyledText5([
38122
- dim4(fg6(PALETTE.dimText)(" ")),
38123
- fg6(PALETTE.bodyText)(`Subagent \xB7 ${lineagePathLabel(attribution.path)}`)
39006
+ rendered.push(new StyledText6([
39007
+ dim5(fg7(PALETTE.dimText)(" ")),
39008
+ fg7(PALETTE.bodyText)(`Subagent \xB7 ${lineagePathLabel(attribution.path)}`)
38124
39009
  ]));
38125
39010
  }
38126
39011
  activeOwnerKey = attribution.ownerKey;
@@ -38147,11 +39032,11 @@ function renderExpandedActivityStep({ item, count, inFlight, nested }) {
38147
39032
  const disclosure = renderActivityDisclosure(item.card);
38148
39033
  const title = expandedActivityTitle(item.card);
38149
39034
  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)(`
39035
+ const step = new StyledText6([
39036
+ fg7(color)(`${indent}${glyph} `),
39037
+ fg7(PALETTE.bodyText)(title),
39038
+ ...count > 1 ? [dim5(fg7(PALETTE.dimText)(` \xD7${count}`))] : [],
39039
+ ...showSummary ? [dim5(fg7(PALETTE.dimText)(`
38155
39040
  ${indent} ${item.card.summary}`))] : []
38156
39041
  ]);
38157
39042
  return disclosure ? joinStyled([step, disclosure], `
@@ -38178,23 +39063,23 @@ function renderActivityDisclosure(card) {
38178
39063
  const details = [];
38179
39064
  if (card.detail !== undefined) {
38180
39065
  const safeDetail = terminalSafeText(card.detail);
38181
- details.push(card.detailFormat === "code" ? renderCodeSnippet({ text: safeDetail, indent: " " }) : new StyledText5([dim4(fg6(PALETTE.dimText)(` ${safeDetail}`))]));
39066
+ details.push(card.detailFormat === "code" ? renderCodeSnippet({ text: safeDetail, indent: " " }) : new StyledText6([dim5(fg7(PALETTE.dimText)(` ${safeDetail}`))]));
38182
39067
  }
38183
39068
  return details.length === 0 ? undefined : joinStyled(details, `
38184
39069
 
38185
39070
  `);
38186
39071
  }
38187
39072
  function renderActivitySummary({ items, includeTimestamp }) {
38188
- return new StyledText5([
39073
+ return new StyledText6([
38189
39074
  ...includeTimestamp ? [renderTimestampChunk(items[0].occurredAt)] : [],
38190
- dim4(fg6(PALETTE.dimText)(`Worked \xB7 ${items.length} step${items.length === 1 ? "" : "s"} \xB7 Ctrl+O details`))
39075
+ dim5(fg7(PALETTE.dimText)(`Worked \xB7 ${items.length} step${items.length === 1 ? "" : "s"} \xB7 Ctrl+O details`))
38191
39076
  ]);
38192
39077
  }
38193
39078
  function renderCollapsedActivityStep({ item, inFlight }) {
38194
39079
  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)))
39080
+ return new StyledText6([
39081
+ fg7(color)(` ${glyph} `),
39082
+ dim5(fg7(PALETTE.dimText)(collapsedActivityTitle(item.card)))
38198
39083
  ]);
38199
39084
  }
38200
39085
  function collapsedActivityTitle(card) {
@@ -38211,15 +39096,15 @@ function collapsedActivityTitle(card) {
38211
39096
  function renderSubmittedTimeline({ state, admittedSubmissions }) {
38212
39097
  const submitted = admittedSubmissions.filter((submission) => submission.status === "submitted" && !state.transcript.some((item) => item.kind === "message" && item.messageId === submission.messageId));
38213
39098
  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([
39099
+ const attachments = renderAttachmentSummary(submission.attachments);
39100
+ const header = new StyledText6([
38216
39101
  renderRoleLabel({ label: "You", role: "human" }),
38217
- dim4(fg6(PALETTE.dimText)(" (CLI)"))
39102
+ dim5(fg7(PALETTE.dimText)(" (CLI)"))
38218
39103
  ]);
38219
39104
  return joinStyled([
38220
39105
  header,
38221
39106
  renderMessageBody({ text: submission.text, role: "human" }),
38222
- ...attachments ? [new StyledText5([attachments])] : []
39107
+ ...attachments.chunks.length > 0 ? [attachments] : []
38223
39108
  ], `
38224
39109
  `);
38225
39110
  });
@@ -38229,20 +39114,20 @@ function renderSubmittedTimeline({ state, admittedSubmissions }) {
38229
39114
  }
38230
39115
  function renderWorkingIndicator({ frame, elapsedMs, mode }) {
38231
39116
  if (mode === "stopping") {
38232
- return new StyledText5([
38233
- fg6(PALETTE.approvalQuestion)("\u23F9"),
38234
- dim4(fg6(PALETTE.dimText)(` Stopping\u2026 ${formatElapsed(elapsedMs)}`))
39117
+ return new StyledText6([
39118
+ fg7(PALETTE.approvalQuestion)("\u23F9"),
39119
+ dim5(fg7(PALETTE.dimText)(` Stopping\u2026 ${formatElapsed(elapsedMs)}`))
38235
39120
  ]);
38236
39121
  }
38237
39122
  if (mode === "complete" || mode === "cancel") {
38238
- return new StyledText5([
38239
- fg6(PALETTE.approvalQuestion)(frame),
38240
- dim4(fg6(PALETTE.dimText)(` ${lifecycleOperationPresentParticiple(mode)} the session\u2026`))
39123
+ return new StyledText6([
39124
+ fg7(PALETTE.approvalQuestion)(frame),
39125
+ dim5(fg7(PALETTE.dimText)(` ${lifecycleOperationPresentParticiple(mode)} the session\u2026`))
38241
39126
  ]);
38242
39127
  }
38243
- return new StyledText5([
38244
- fg6(PALETTE.remyAccent)(frame),
38245
- dim4(fg6(PALETTE.dimText)(` Working ${formatElapsed(elapsedMs)}`))
39128
+ return new StyledText6([
39129
+ fg7(PALETTE.remyAccent)(frame),
39130
+ dim5(fg7(PALETTE.dimText)(` Working ${formatElapsed(elapsedMs)}`))
38246
39131
  ]);
38247
39132
  }
38248
39133
  function lifecycleOperationPresentParticiple(operation) {
@@ -38264,22 +39149,22 @@ function truncate2(value, width) {
38264
39149
  }
38265
39150
  function renderComposerStatus({ admittedSubmissions }) {
38266
39151
  if (admittedSubmissions.length === 0)
38267
- return new StyledText5([]);
39152
+ return new StyledText6([]);
38268
39153
  const parts = admittedSubmissions.flatMap((submission) => {
38269
39154
  if (submission.status === "submitted")
38270
39155
  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
- ])];
39156
+ const attachments = renderAttachmentSummary(submission.attachments);
39157
+ return [submission.status === "failed" ? joinStyled([
39158
+ new StyledText6([bold4(fg7(PALETTE.failure)("Failed: ")), fg7(PALETTE.bodyText)(submission.error ?? "")]),
39159
+ renderMessageBody({ text: submission.text, role: "human" }),
39160
+ ...attachments.chunks.length > 0 ? [attachments] : [],
39161
+ new StyledText6([dim5(fg7(PALETTE.dimText)("Ctrl+R retries oldest failed submission."))])
39162
+ ], `
39163
+ `) : joinStyled([
39164
+ new StyledText6([fg7(PALETTE.progress)("Admitting: "), fg7(PALETTE.bodyText)(submission.text)]),
39165
+ ...attachments.chunks.length > 0 ? [attachments] : []
39166
+ ], `
39167
+ `)];
38283
39168
  });
38284
39169
  return joinStyled(parts, `
38285
39170
 
@@ -38290,10 +39175,15 @@ async function createDefaultRenderer3() {
38290
39175
  }
38291
39176
 
38292
39177
  // src/tui/attachments.ts
39178
+ import { Buffer as Buffer2 } from "buffer";
38293
39179
  import { randomUUID as randomUUID4 } from "crypto";
38294
- import { writeFile as writeFile3, stat } from "fs/promises";
38295
- import { homedir as homedir2 } from "os";
39180
+ import { mkdtemp, open as open4, readdir as readdir2, rm, stat, writeFile as writeFile3 } from "fs/promises";
39181
+ import { homedir as homedir2, tmpdir } from "os";
38296
39182
  import { basename as basename3, isAbsolute as isAbsolute2, join as join5, resolve as resolve2 } from "path";
39183
+ var directoryArchiveMediaType = "application/gzip";
39184
+ var directoryArchiveSuffix = ".tar.gz";
39185
+ var maxPortableFilenameBytes = 255;
39186
+ var attachmentSizeLimitLabel = `${fileUploadMaxBytes / (1024 * 1024)} MiB`;
38297
39187
  async function readClipboardImage() {
38298
39188
  if (process.platform !== "darwin")
38299
39189
  throw new Error("Clipboard image paste is supported on macOS only.");
@@ -38319,11 +39209,13 @@ async function attachPromptPaths({
38319
39209
  text,
38320
39210
  cwd,
38321
39211
  excludedPaths = [],
38322
- reserveAndUploadFile: reserveAndUploadFile2
39212
+ selectedPaths = [],
39213
+ reserveAndUploadFile: reserveAndUploadFile2,
39214
+ onUploadStart
38323
39215
  }) {
38324
39216
  const excluded = new Set(excludedPaths);
38325
- const attachments = [];
38326
- for (const candidate of promptFilePaths({ text, cwd })) {
39217
+ const pathsToUpload = [];
39218
+ for (const candidate of promptFilePaths({ text, cwd, selectedPaths })) {
38327
39219
  const { filePath } = candidate;
38328
39220
  if (excluded.has(filePath))
38329
39221
  continue;
@@ -38335,27 +39227,168 @@ async function attachPromptPaths({
38335
39227
  continue;
38336
39228
  throw error93;
38337
39229
  }
38338
- if (!file3.isFile() && candidate.explicit)
38339
- throw new Error(`Prompt attachment path is not a regular file: ${filePath}`);
38340
- if (!file3.isFile())
39230
+ if (!file3.isFile() && !file3.isDirectory() && candidate.explicit)
39231
+ throw new Error(`Prompt attachment path is not a regular file or directory: ${filePath}`);
39232
+ if (!file3.isFile() && !file3.isDirectory())
38341
39233
  continue;
39234
+ pathsToUpload.push(filePath);
39235
+ }
39236
+ if (pathsToUpload.length === 0)
39237
+ return [];
39238
+ onUploadStart?.({ filenames: pathsToUpload.map((filePath) => basename3(filePath)) });
39239
+ const attachments = [];
39240
+ for (const filePath of pathsToUpload) {
39241
+ const uploaded = await uploadLocalAttachment({ filePath, reserveAndUploadFile: reserveAndUploadFile2 });
38342
39242
  attachments.push({
38343
39243
  id: randomUUID4(),
38344
- fileId: await reserveAndUploadFile2({ filePath }),
38345
- filename: basename3(filePath),
38346
- sourcePath: filePath
39244
+ ...uploaded
38347
39245
  });
38348
39246
  }
38349
39247
  return attachments;
38350
39248
  }
38351
- function promptFilePaths({ text, cwd }) {
39249
+ async function uploadLocalAttachment({
39250
+ filePath,
39251
+ reserveAndUploadFile: reserveAndUploadFile2
39252
+ }) {
39253
+ const sourcePath = resolve2(filePath);
39254
+ const source = await stat(sourcePath);
39255
+ if (source.isFile()) {
39256
+ if (source.size > fileUploadMaxBytes)
39257
+ throw oversizedFileAttachmentError(sourcePath);
39258
+ return {
39259
+ fileId: await reserveAndUploadFile2({ filePath: sourcePath }),
39260
+ filename: basename3(sourcePath),
39261
+ sourcePath
39262
+ };
39263
+ }
39264
+ if (!source.isDirectory())
39265
+ throw new Error(`Attachment path is not a regular file or directory: ${sourcePath}`);
39266
+ const archive = await createDirectoryArchive(sourcePath);
39267
+ try {
39268
+ return {
39269
+ fileId: await reserveAndUploadFile2({ filePath: archive.path, mediaType: directoryArchiveMediaType }),
39270
+ filename: archive.filename,
39271
+ sourcePath
39272
+ };
39273
+ } finally {
39274
+ await rm(archive.temporaryDirectory, { recursive: true, force: true });
39275
+ }
39276
+ }
39277
+ async function createDirectoryArchive(directoryPath) {
39278
+ const rootName = basename3(directoryPath) || "directory";
39279
+ const files = Object.create(null);
39280
+ await collectDirectoryFiles({
39281
+ directoryPath,
39282
+ archivePath: rootName,
39283
+ attachmentDirectoryPath: directoryPath,
39284
+ collectedByteSize: 0,
39285
+ files
39286
+ });
39287
+ if (Object.keys(files).length === 0)
39288
+ throw new Error(`Directory attachment contains no regular files: ${directoryPath}`);
39289
+ const temporaryDirectory = await mkdtemp(join5(tmpdir(), "remy-directory-attachment-"));
39290
+ const filename = directoryArchiveFilename(rootName);
39291
+ const archivePath = join5(temporaryDirectory, filename);
39292
+ try {
39293
+ await Bun.Archive.write(archivePath, files, { compress: "gzip" });
39294
+ if ((await stat(archivePath)).size > fileUploadMaxBytes)
39295
+ throw oversizedDirectoryArchiveError(directoryPath);
39296
+ } catch (error93) {
39297
+ await rm(temporaryDirectory, { recursive: true, force: true });
39298
+ throw error93;
39299
+ }
39300
+ return { path: archivePath, filename, temporaryDirectory };
39301
+ }
39302
+ async function collectDirectoryFiles({
39303
+ directoryPath,
39304
+ archivePath,
39305
+ attachmentDirectoryPath,
39306
+ collectedByteSize,
39307
+ files
39308
+ }) {
39309
+ const entries = await readdir2(directoryPath, { withFileTypes: true });
39310
+ entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
39311
+ let byteSize = collectedByteSize;
39312
+ for (const entry of entries) {
39313
+ const entryPath = join5(directoryPath, entry.name);
39314
+ const entryArchivePath = `${archivePath}/${entry.name}`;
39315
+ if (entry.isDirectory()) {
39316
+ byteSize = await collectDirectoryFiles({
39317
+ directoryPath: entryPath,
39318
+ archivePath: entryArchivePath,
39319
+ attachmentDirectoryPath,
39320
+ collectedByteSize: byteSize,
39321
+ files
39322
+ });
39323
+ continue;
39324
+ }
39325
+ if (entry.isFile()) {
39326
+ const fileHandle = await open4(entryPath, "r");
39327
+ try {
39328
+ const entryStat = await fileHandle.stat();
39329
+ if (!entryStat.isFile())
39330
+ throw new Error(`Directory attachment contains a non-regular entry: ${entryPath}`);
39331
+ const remainingBytes = fileUploadMaxBytes - byteSize;
39332
+ if (entryStat.size > remainingBytes)
39333
+ throw oversizedDirectoryAttachmentError(attachmentDirectoryPath);
39334
+ const readResult = await readFileHandleWithLimit({
39335
+ fileHandle,
39336
+ initialByteSize: entryStat.size,
39337
+ maxBytes: remainingBytes
39338
+ });
39339
+ if (readResult.status === "limit_exceeded")
39340
+ throw oversizedDirectoryAttachmentError(attachmentDirectoryPath);
39341
+ files[entryArchivePath] = readResult.bytes;
39342
+ byteSize += readResult.bytes.byteLength;
39343
+ } finally {
39344
+ await fileHandle.close();
39345
+ }
39346
+ continue;
39347
+ }
39348
+ if (entry.isSymbolicLink())
39349
+ throw new Error(`Directory attachment contains a symbolic link: ${entryPath}`);
39350
+ throw new Error(`Directory attachment contains a non-regular entry: ${entryPath}`);
39351
+ }
39352
+ return byteSize;
39353
+ }
39354
+ function oversizedFileAttachmentError(filePath) {
39355
+ return new Error(`Attachment file ${JSON.stringify(filePath)} is larger than ${attachmentSizeLimitLabel}. Choose a file no larger than ${attachmentSizeLimitLabel}.`);
39356
+ }
39357
+ function oversizedDirectoryAttachmentError(directoryPath) {
39358
+ return new Error(`Attachment directory ${JSON.stringify(directoryPath)} contains more than ${attachmentSizeLimitLabel} of files. Choose fewer or smaller files.`);
39359
+ }
39360
+ function oversizedDirectoryArchiveError(directoryPath) {
39361
+ return new Error(`Attachment directory ${JSON.stringify(directoryPath)} produces an archive larger than ${attachmentSizeLimitLabel}. Choose fewer or smaller files.`);
39362
+ }
39363
+ function directoryArchiveFilename(directoryName) {
39364
+ const stem = truncateUtf8(directoryName, maxPortableFilenameBytes - Buffer2.byteLength(directoryArchiveSuffix)) || "directory";
39365
+ return `${stem}${directoryArchiveSuffix}`;
39366
+ }
39367
+ function truncateUtf8(value, maxBytes) {
39368
+ let result = "";
39369
+ let resultBytes = 0;
39370
+ for (const character of value) {
39371
+ const characterBytes = Buffer2.byteLength(character);
39372
+ if (resultBytes + characterBytes > maxBytes)
39373
+ break;
39374
+ result += character;
39375
+ resultBytes += characterBytes;
39376
+ }
39377
+ return result;
39378
+ }
39379
+ function promptFilePaths({ text, cwd, selectedPaths }) {
38352
39380
  const paths = new Map;
39381
+ for (const selectedPath of selectedPaths)
39382
+ paths.set(isAbsolute2(selectedPath) ? selectedPath : resolve2(cwd, selectedPath), true);
38353
39383
  for (const rawToken of text.split(/\s+/)) {
38354
39384
  const token = rawToken.replace(/^[([{'"`]+/, "").replace(/[),.;:!?\]}"`]+$/, "");
38355
39385
  const pathText = token.startsWith("@") ? token.slice(1) : token;
38356
39386
  if (!pathText)
38357
39387
  continue;
38358
39388
  const explicit = token.startsWith("@") || pathText.startsWith("./") || pathText.startsWith("../") || isAbsolute2(pathText) || pathText.startsWith("~/");
39389
+ const pathShaped = explicit || pathText.includes("/") || pathText.includes("\\") || pathText.includes(".");
39390
+ if (!pathShaped)
39391
+ continue;
38359
39392
  const expandedPath = pathText.startsWith("~/") ? join5(homedir2(), pathText.slice(2)) : pathText;
38360
39393
  const filePath = isAbsolute2(expandedPath) ? expandedPath : resolve2(cwd, expandedPath);
38361
39394
  paths.set(filePath, paths.get(filePath) || explicit);
@@ -38378,7 +39411,7 @@ function isPng(bytes) {
38378
39411
  }
38379
39412
 
38380
39413
  // 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";
39414
+ import { bold as bold5, BoxRenderable as BoxRenderable5, fg as fg8, NativeImage, StyledText as StyledText7, TextRenderable as TextRenderable5, bg as bg5 } from "@opentui/core";
38382
39415
  // src/tui/remy-mark.ts
38383
39416
  var remyPixelFieldSource = new URL("../assets/remy-pixel-field.png", import.meta.url);
38384
39417
  function shouldShowRemySplash({ width, height }) {
@@ -38397,7 +39430,7 @@ var compactMarkRows = 9;
38397
39430
  var compactMinWidth = 48;
38398
39431
  var compactMinHeight = 20;
38399
39432
  var markBrightnessGain = 4.2;
38400
- var remyCliVersion = "1.13.0";
39433
+ var remyCliVersion = "1.14.1";
38401
39434
  async function showRemySplash({
38402
39435
  createRenderer = createRemyRenderer,
38403
39436
  durationMs = splashDurationMs,
@@ -38455,7 +39488,7 @@ async function showRemySplash({
38455
39488
  });
38456
39489
  const approvalCopy = new TextRenderable5(renderer, { content: "" });
38457
39490
  const metadata = new TextRenderable5(renderer, {
38458
- content: new StyledText6([fg7(PALETTE.dimText)(`v${remyCliVersion} \xB7 Tela\xAE`)])
39491
+ content: new StyledText7([fg8(PALETTE.dimText)(`v${remyCliVersion} \xB7 Tela\xAE`)])
38459
39492
  });
38460
39493
  const field = await loadPixelFieldUntilAbort({ signal });
38461
39494
  if (!field || signal?.aborted)
@@ -38553,32 +39586,32 @@ function createSplashController({
38553
39586
  };
38554
39587
  }
38555
39588
  function renderBootstrapActions({ state, frame }) {
38556
- return new StyledText6([
39589
+ return new StyledText7([
38557
39590
  actionText({ label: state.authenticatedEmail ? `Authenticated as ${state.authenticatedEmail}` : "Checking authentication...", status: state.authentication, frame }),
38558
- fg7(PALETTE.dimText)(`
39591
+ fg8(PALETTE.dimText)(`
38559
39592
  `),
38560
39593
  actionText({ label: "Load sessions", status: state.loadingSessions, frame })
38561
39594
  ]);
38562
39595
  }
38563
39596
  function actionText({ label, status, frame }) {
38564
39597
  if (status === "complete")
38565
- return fg7(PALETTE.statusCompleted)(`\u2713 ${label === "Load sessions" ? "Sessions loaded!" : label}`);
39598
+ return fg8(PALETTE.statusCompleted)(`\u2713 ${label === "Load sessions" ? "Sessions loaded!" : label}`);
38566
39599
  if (status === "needed")
38567
- return fg7(PALETTE.approvalQuestion)("\u26A0 Authentication needed");
39600
+ return fg8(PALETTE.approvalQuestion)("\u26A0 Authentication needed");
38568
39601
  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}`);
39602
+ return fg8(PALETTE.progress)(`${splashSpinnerFrames[frame % splashSpinnerFrames.length]} ${label === "Load sessions" ? "Loading sessions..." : label}`);
39603
+ return fg8(PALETTE.dimText)(`\u25CB ${label}`);
38571
39604
  }
38572
39605
  function renderAuthenticationBand({ state, frame }) {
38573
39606
  if (!state.waitingForAuthentication)
38574
- return new StyledText6([]);
39607
+ return new StyledText7([]);
38575
39608
  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."),
39609
+ const url3 = state.verificationUrl ? [fg8(PALETTE.dimText)(`
39610
+ Verification URL: `), bold5(fg8(PALETTE.humanAccent)(state.verificationUrl))] : [];
39611
+ return new StyledText7([
39612
+ fg8(PALETTE.bodyText)("Continue authentication in your browser."),
38580
39613
  ...url3,
38581
- fg7(PALETTE.dimText)(`
39614
+ fg8(PALETTE.dimText)(`
38582
39615
  ${spinner} Waiting for approval \xB7 q / esc cancel`)
38583
39616
  ]);
38584
39617
  }
@@ -38653,7 +39686,7 @@ function renderMark(field, frame, rows = markRows) {
38653
39686
  const flush = () => {
38654
39687
  if (runLength === 0)
38655
39688
  return;
38656
- chunks.push(fg7(grey(runTop))(bg5(grey(runBottom))(upperHalfBlock.repeat(runLength))));
39689
+ chunks.push(fg8(grey(runTop))(bg5(grey(runBottom))(upperHalfBlock.repeat(runLength))));
38657
39690
  runLength = 0;
38658
39691
  };
38659
39692
  for (let column = 0;column < columns; column++) {
@@ -38670,10 +39703,10 @@ function renderMark(field, frame, rows = markRows) {
38670
39703
  }
38671
39704
  flush();
38672
39705
  if (row < rows - 1)
38673
- chunks.push(fg7("#000000")(`
39706
+ chunks.push(fg8("#000000")(`
38674
39707
  `));
38675
39708
  }
38676
- return new StyledText6(chunks);
39709
+ return new StyledText7(chunks);
38677
39710
  }
38678
39711
  function shimmerBrightness(field, column, sampleRow, columns, sampleRows, center) {
38679
39712
  const x0 = Math.floor(column / columns * field.width);
@@ -38693,8 +39726,8 @@ function grey(value) {
38693
39726
  return `#${hex5}${hex5}${hex5}`;
38694
39727
  }
38695
39728
  function renderSplashTitle() {
38696
- return new StyledText6([
38697
- bold5(fg7(PALETTE.bodyText)("Remy CLI"))
39729
+ return new StyledText7([
39730
+ bold5(fg8(PALETTE.bodyText)("Remy CLI"))
38698
39731
  ]);
38699
39732
  }
38700
39733
 
@@ -39029,6 +40062,9 @@ function createRepositoryLister({ client }) {
39029
40062
  function createRepositorySuggester({ client }) {
39030
40063
  return async (input) => await suggestRemoteRepositories({ client, input });
39031
40064
  }
40065
+ function createBranchSuggester({ client }) {
40066
+ return async (input) => await suggestRemoteBranches({ client, input });
40067
+ }
39032
40068
  function parseOptionalStringFlag(value, name) {
39033
40069
  if (value === undefined)
39034
40070
  return;
@@ -39172,48 +40208,98 @@ async function dashboard({
39172
40208
  const openDashboardTui = dependencies.openDashboardTui ?? createDashboardTui;
39173
40209
  const pages = [initialSessions];
39174
40210
  let pageIndex = 0;
39175
- let loadingPage;
40211
+ let filters = { statuses: [], creators: [] };
40212
+ let authors;
40213
+ let activeDashboardGeneration = 0;
39176
40214
  function holdTransitionScreen() {
39177
40215
  showRemyTransitionScreen({ write: (chunk) => dependencies.output.writeStdout(chunk) });
39178
40216
  }
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({
40217
+ function createDashboardPageLoader({ generation }) {
40218
+ const loadingPages = new Map;
40219
+ let pageLoadTail = Promise.resolve();
40220
+ return async (input) => {
40221
+ const requestKey = "filters" in input ? `filters:${JSON.stringify(input.filters)}` : input.target;
40222
+ const matchingPageLoad = loadingPages.get(requestKey);
40223
+ if (matchingPageLoad)
40224
+ return await matchingPageLoad;
40225
+ const pendingPageLoad = pageLoadTail;
40226
+ const pageLoad = pendingPageLoad.then(async () => {
40227
+ if (generation !== activeDashboardGeneration)
40228
+ return { ...pages[pageIndex], canGoPrevious: pageIndex > 0 };
40229
+ if ("filters" in input) {
40230
+ const nextFilters = input.filters;
40231
+ const first = toDashboardPage(await operations.listSessions({
40232
+ client: operations.client,
40233
+ limit: 20,
40234
+ ...nextFilters.statuses.length > 0 ? { statuses: nextFilters.statuses } : {},
40235
+ ...nextFilters.creators.length > 0 ? { creatorUserIds: nextFilters.creators.map((creator) => creator.id) } : {},
40236
+ signal: input.signal
40237
+ }));
40238
+ if (generation !== activeDashboardGeneration)
40239
+ return first;
40240
+ filters = nextFilters;
40241
+ pages.splice(0, pages.length, first);
40242
+ pageIndex = 0;
40243
+ return first;
40244
+ }
40245
+ const { target, signal } = input;
40246
+ if (target === "current") {
40247
+ const after = pageIndex > 0 ? pages[pageIndex - 1]?.nextCursor ?? undefined : undefined;
40248
+ const refreshed = toDashboardPage(await operations.listSessions({
40249
+ client: operations.client,
40250
+ limit: 20,
40251
+ after,
40252
+ ...filters.statuses.length > 0 ? { statuses: filters.statuses } : {},
40253
+ ...filters.creators.length > 0 ? { creatorUserIds: filters.creators.map((creator) => creator.id) } : {},
40254
+ signal
40255
+ }));
40256
+ if (generation !== activeDashboardGeneration)
40257
+ return refreshed;
40258
+ pages.splice(pageIndex, pages.length - pageIndex, refreshed);
40259
+ return { ...refreshed, canGoPrevious: pageIndex > 0 };
40260
+ }
40261
+ if (target === "previous") {
40262
+ pageIndex = Math.max(0, pageIndex - 1);
40263
+ return { ...pages[pageIndex], canGoPrevious: pageIndex > 0 };
40264
+ }
40265
+ const current = pages[pageIndex];
40266
+ if (!current.hasMore || !current.nextCursor)
40267
+ return { ...current, canGoPrevious: pageIndex > 0 };
40268
+ const next = toDashboardPage(await operations.listSessions({
39186
40269
  client: operations.client,
39187
40270
  limit: 20,
39188
- after,
40271
+ after: current.nextCursor,
40272
+ ...filters.statuses.length > 0 ? { statuses: filters.statuses } : {},
40273
+ ...filters.creators.length > 0 ? { creatorUserIds: filters.creators.map((creator) => creator.id) } : {},
39189
40274
  signal
39190
40275
  }));
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 };
40276
+ if (generation !== activeDashboardGeneration)
40277
+ return next;
40278
+ pages.splice(pageIndex + 1);
40279
+ pages.push(next);
40280
+ pageIndex += 1;
40281
+ return { ...next, canGoPrevious: pageIndex > 0 };
40282
+ });
40283
+ loadingPages.set(requestKey, pageLoad);
40284
+ pageLoadTail = pageLoad.then(() => {
40285
+ return;
40286
+ }, () => {
40287
+ return;
40288
+ });
40289
+ try {
40290
+ return await pageLoad;
40291
+ } finally {
40292
+ if (loadingPages.get(requestKey) === pageLoad)
40293
+ loadingPages.delete(requestKey);
39197
40294
  }
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
- }
40295
+ };
40296
+ }
40297
+ async function loadDashboardAuthors() {
40298
+ if (authors)
40299
+ return authors;
40300
+ const loadedAuthors = await collectAllUsers({ listUsers: operations.listUsers, signal: dependencies.abortSignal });
40301
+ authors = loadedAuthors;
40302
+ return loadedAuthors;
39217
40303
  }
39218
40304
  let firstOpen = true;
39219
40305
  holdTransitionScreen();
@@ -39222,17 +40308,30 @@ async function dashboard({
39222
40308
  pages.splice(0, pages.length, toDashboardPage(await operations.listSessions({
39223
40309
  client: operations.client,
39224
40310
  limit: 20,
40311
+ ...filters.statuses.length > 0 ? { statuses: filters.statuses } : {},
40312
+ ...filters.creators.length > 0 ? { creatorUserIds: filters.creators.map((creator) => creator.id) } : {},
39225
40313
  signal: dependencies.abortSignal
39226
40314
  })));
39227
40315
  pageIndex = 0;
39228
40316
  }
39229
40317
  firstOpen = false;
40318
+ const dashboardGeneration = ++activeDashboardGeneration;
40319
+ const loadDashboardPage = createDashboardPageLoader({ generation: dashboardGeneration });
39230
40320
  const action = await renderRemyView({
39231
40321
  open: async () => await openDashboardTui({
39232
40322
  initialPage: pages[0],
40323
+ initialFilters: filters,
40324
+ loadAuthors: loadDashboardAuthors,
39233
40325
  loadPage: loadDashboardPage
39234
40326
  }),
39235
- waitForAction: async (dashboardTui) => await awaitWithAbort({ value: dashboardTui.waitForAction(), abortSignal: dependencies.abortSignal })
40327
+ waitForAction: async (dashboardTui) => {
40328
+ try {
40329
+ return await awaitWithAbort({ value: dashboardTui.waitForAction(), abortSignal: dependencies.abortSignal });
40330
+ } finally {
40331
+ if (activeDashboardGeneration === dashboardGeneration)
40332
+ activeDashboardGeneration += 1;
40333
+ }
40334
+ }
39236
40335
  });
39237
40336
  if (!action)
39238
40337
  return 0;
@@ -39275,12 +40374,12 @@ async function prepareDashboardBootstrap({
39275
40374
  return;
39276
40375
  });
39277
40376
  onLoadingSessions?.();
39278
- const initialSessions = toDashboardPage(await resolvedOperations.listSessions({
40377
+ const initialSessionPage = await resolvedOperations.listSessions({
39279
40378
  client: resolvedOperations.client,
39280
40379
  limit: 20,
39281
40380
  signal: dependencies.abortSignal
39282
- }));
39283
- return { operations: resolvedOperations, repositories, initialSessions };
40381
+ });
40382
+ return { operations: resolvedOperations, repositories, initialSessions: toDashboardPage(initialSessionPage) };
39284
40383
  }
39285
40384
  async function prepareDashboardWithStartupSplash({
39286
40385
  dependencies,
@@ -39471,11 +40570,15 @@ async function createNewSession({
39471
40570
  throw new Error("All selected repositories must belong to the same GitHub installation.");
39472
40571
  const fileIds = [];
39473
40572
  for (const attachment of command.attachments) {
39474
- fileIds.push(await operations.reserveAndUploadFile({
39475
- client: operations.client,
40573
+ const uploaded = await uploadLocalAttachment({
39476
40574
  filePath: attachment,
39477
- idempotencyKey: randomUUID5()
39478
- }));
40575
+ reserveAndUploadFile: async (input) => await operations.reserveAndUploadFile({
40576
+ ...input,
40577
+ client: operations.client,
40578
+ idempotencyKey: randomUUID5()
40579
+ })
40580
+ });
40581
+ fileIds.push(uploaded.fileId);
39479
40582
  }
39480
40583
  const created = await operations.createCodexSession({
39481
40584
  client: operations.client,
@@ -39549,6 +40652,7 @@ async function createFromTuiDraft({
39549
40652
  input: {
39550
40653
  installationId: draft.githubInstallationId,
39551
40654
  repositoryIds: draft.repositories.map((repository) => repository.id),
40655
+ repositoryBranchOverrides: draft.repositoryBranchOverrides ?? [],
39552
40656
  prompt: draft.prompt,
39553
40657
  fileIds: draft.attachments.map((attachment) => attachment.fileId),
39554
40658
  model: draft.model,
@@ -39571,10 +40675,11 @@ async function openTuiNewSessionWizard({
39571
40675
  repositories,
39572
40676
  reloadRepositories,
39573
40677
  suggestRepositories: operations.suggestRepositories,
40678
+ suggestBranches: operations.suggestBranches,
39574
40679
  initialDraft,
39575
40680
  initialError,
39576
40681
  readClipboardImage,
39577
- savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir() }),
40682
+ savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir2() }),
39578
40683
  attachPromptPaths: async (input) => await uploadPromptPaths({ operations, ...input })
39579
40684
  }),
39580
40685
  waitForAction: async (wizard) => await awaitWithAbort({ value: wizard.waitForDraft(), abortSignal: dependencies.abortSignal })
@@ -39583,15 +40688,20 @@ async function openTuiNewSessionWizard({
39583
40688
  async function uploadPromptPaths({
39584
40689
  operations,
39585
40690
  text,
39586
- excludedPaths
40691
+ excludedPaths,
40692
+ selectedPaths,
40693
+ onUploadStart
39587
40694
  }) {
39588
40695
  return await attachPromptPaths({
39589
40696
  text,
39590
40697
  excludedPaths,
40698
+ selectedPaths,
40699
+ onUploadStart,
39591
40700
  cwd: process.cwd(),
39592
- reserveAndUploadFile: async ({ filePath }) => await operations.reserveAndUploadFile({
40701
+ reserveAndUploadFile: async ({ filePath, mediaType }) => await operations.reserveAndUploadFile({
39593
40702
  client: operations.client,
39594
40703
  filePath,
40704
+ ...mediaType ? { mediaType } : {},
39595
40705
  idempotencyKey: randomUUID5()
39596
40706
  })
39597
40707
  });
@@ -39649,7 +40759,7 @@ async function runAttachedSession({
39649
40759
  ...activeMessageId ? { activeMessageId } : {},
39650
40760
  environment: dependencies.environment,
39651
40761
  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 }),
40762
+ listSessionEvents: async ({ sessionId: id, limit, after, signal }) => await operations.listSessionEvents({ client: operations.client, sessionId: id, limit, after, signal }),
39653
40763
  openEventStream: ({ sessionId: id, lastRetainedEventId, signal, onSynchronized }) => operations.streamSessionEvents({ client: operations.client, sessionId: id, lastRetainedEventId, signal, onSynchronized })
39654
40764
  });
39655
40765
  const interactive = !noTui && !json3 && isInteractiveTerminal(dependencies);
@@ -39688,8 +40798,14 @@ async function runAttachedSession({
39688
40798
  });
39689
40799
  }
39690
40800
  const startPromise = controller.start(start);
39691
- await Promise.race([controller.waitUntilReady(), startPromise]);
40801
+ await Promise.race([
40802
+ controller.waitUntilReady(),
40803
+ startPromise,
40804
+ ...aborted3 ? [aborted3] : []
40805
+ ]);
39692
40806
  if (interactive) {
40807
+ if (dependencies.abortSignal?.aborted)
40808
+ throw dependencies.abortSignal.reason ?? new Error("interrupted");
39693
40809
  const openSessionTui = dependencies.openSessionTui ?? createSessionTui;
39694
40810
  let composerSnapshot;
39695
40811
  const holdTransitionScreen = () => showRemyTransitionScreen({ write: (chunk) => dependencies.output.writeStdout(chunk) });
@@ -39726,7 +40842,7 @@ async function runAttachedSession({
39726
40842
  controller.updateDetail(detail);
39727
40843
  },
39728
40844
  readClipboardImage,
39729
- savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir() }),
40845
+ savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir2() }),
39730
40846
  attachPromptPaths: async (input) => await uploadPromptPaths({ operations, ...input })
39731
40847
  }),
39732
40848
  waitForAction: async (tui) => await Promise.race([tui.waitForAction(), aborted3])
@@ -39835,7 +40951,9 @@ async function createSessionOperations(dependencies, { onAuthenticated } = {}) {
39835
40951
  return {
39836
40952
  client: dependencies.sessionClient,
39837
40953
  listRepositories: dependencies.listRepositories ?? (async () => await Promise.reject(new Error("Missing remote repository operation."))),
40954
+ listUsers: dependencies.listUsers ?? (async (input) => await listRemoteUsers({ client: dependencies.sessionClient, ...input })),
39838
40955
  suggestRepositories: dependencies.suggestRepositories ?? (async () => await Promise.reject(new Error("Missing remote repository suggestions operation."))),
40956
+ suggestBranches: dependencies.suggestBranches ?? (async () => await Promise.reject(new Error("Missing remote branch suggestions operation."))),
39839
40957
  reserveAndUploadFile: dependencies.reserveAndUploadFile ?? reserveAndUploadFile,
39840
40958
  createCodexSession: dependencies.createCodexSession ?? createCodexSession,
39841
40959
  appendSessionMessage: dependencies.appendSessionMessage ?? appendSessionMessage,
@@ -39853,7 +40971,9 @@ async function createSessionOperations(dependencies, { onAuthenticated } = {}) {
39853
40971
  return {
39854
40972
  client,
39855
40973
  listRepositories: dependencies.listRepositories ?? createRepositoryLister({ client }),
40974
+ listUsers: dependencies.listUsers ?? (async (input) => await listRemoteUsers({ client, ...input })),
39856
40975
  suggestRepositories: dependencies.suggestRepositories ?? createRepositorySuggester({ client }),
40976
+ suggestBranches: dependencies.suggestBranches ?? createBranchSuggester({ client }),
39857
40977
  reserveAndUploadFile: dependencies.reserveAndUploadFile ?? reserveAndUploadFile,
39858
40978
  createCodexSession: dependencies.createCodexSession ?? createCodexSession,
39859
40979
  appendSessionMessage: dependencies.appendSessionMessage ?? appendSessionMessage,
@@ -39895,6 +41015,20 @@ async function collectAllRepositories({
39895
41015
  after = page.nextCursor;
39896
41016
  }
39897
41017
  }
41018
+ async function collectAllUsers({
41019
+ listUsers,
41020
+ signal
41021
+ }) {
41022
+ const users = [];
41023
+ let after;
41024
+ while (true) {
41025
+ const page = await listUsers({ limit: 100, after, signal });
41026
+ users.push(...page.data);
41027
+ if (!page.hasMore || !page.nextCursor)
41028
+ return users;
41029
+ after = page.nextCursor;
41030
+ }
41031
+ }
39898
41032
  function resolveSessionCachePathForCommand({ dependencies, sessionId }) {
39899
41033
  const stateBase = dependencies.environment.XDG_STATE_HOME ?? (dependencies.environment.HOME ? join6(dependencies.environment.HOME, ".local/state") : undefined);
39900
41034
  if (!stateBase)
@@ -40021,7 +41155,7 @@ Options:
40021
41155
  --installation <id> Select a GitHub installation
40022
41156
  --model <name> Select an agent model
40023
41157
  --reasoning-effort <low|medium|high|xhigh> Select agent reasoning effort
40024
- --attach <path> Upload a file; repeat for more attachments
41158
+ --attach <path> Upload a file or directory (20 MiB max); repeat as needed
40025
41159
  --no-tui Do not open the interactive terminal view
40026
41160
  --json Write session updates as JSON
40027
41161
  --help, -h Show this help
@@ -40170,7 +41304,7 @@ async function removeCommittedTokenRecoveryArtifacts({
40170
41304
  const directory = dirname6(tokenPath);
40171
41305
  let entries;
40172
41306
  try {
40173
- entries = await readdir2(directory);
41307
+ entries = await readdir3(directory);
40174
41308
  } catch (error93) {
40175
41309
  if (isMissingFileError3(error93))
40176
41310
  return;