@meistrari/remy-cli 1.12.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +20 -7
  2. package/dist/remy.js +2094 -432
  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 });
@@ -33925,15 +34086,16 @@ var sessionWorkspaceGitRevisionEventSchema = exports_external2.object({
33925
34086
  trigger: exports_external2.enum(["turn-ended", "boot-reconcile"])
33926
34087
  }).passthrough()
33927
34088
  }).passthrough();
33928
- var agentTurnEndedEventSchema = exports_external2.object({
33929
- type: exports_external2.literal("agent.turn.ended"),
33930
- turnId: exports_external2.string().min(1),
33931
- actor: exports_external2.object({ type: exports_external2.enum(["main", "subagent"]) }).passthrough(),
33932
- payload: exports_external2.object({ status: exports_external2.enum(["completed", "failed", "cancelled", "interrupted"]) }).passthrough()
34089
+ var agentTurnEndedEventSchema = wrappedTurnEndedAgentEventSchema.pick({
34090
+ type: true,
34091
+ turnId: true,
34092
+ actor: true,
34093
+ payload: true
33933
34094
  }).passthrough();
33934
- var agentMessageDeltaEventSchema = exports_external2.object({
33935
- type: exports_external2.literal("agent.message.delta"),
33936
- payload: exports_external2.object({ role: exports_external2.string(), delta: exports_external2.string() }).passthrough()
34095
+ var agentMessageDeltaEventSchema = wrappedMessageDeltaAgentEventSchema.pick({
34096
+ type: true,
34097
+ actor: true,
34098
+ payload: true
33937
34099
  }).passthrough();
33938
34100
  var publicMessageContentSegmentSchema = exports_external2.discriminatedUnion("type", [
33939
34101
  exports_external2.strictObject({ type: exports_external2.literal("text"), text: exports_external2.string() }),
@@ -33970,7 +34132,9 @@ function createSessionViewState({ detail, activeMessageId }) {
33970
34132
  kind: "session-view",
33971
34133
  sessionId: detail.id,
33972
34134
  ...detail.sessionNumber === undefined ? {} : { sessionNumber: detail.sessionNumber },
34135
+ title: detail.title ?? null,
33973
34136
  aggregateStatus: detail.status,
34137
+ ...detail.agentStatus === undefined ? {} : { agentStatus: detail.agentStatus },
33974
34138
  connectionStatus: "connected",
33975
34139
  pullRequest: toSessionPullRequest(detail),
33976
34140
  ...activeMessageId !== undefined ? { activeMessageId } : {},
@@ -33985,6 +34149,13 @@ function createSessionViewState({ detail, activeMessageId }) {
33985
34149
  messageTurns: {},
33986
34150
  retainedTurnStarts: {},
33987
34151
  retainedTurnEnds: {},
34152
+ childLineage: {
34153
+ nextOrder: 0,
34154
+ runtimeEpochs: {},
34155
+ seenRuntimeBoundaryEventIds: {},
34156
+ facts: [],
34157
+ owners: []
34158
+ },
33988
34159
  context: {
33989
34160
  snapshot: { status: "unknown" },
33990
34161
  through: 0
@@ -33996,7 +34167,9 @@ function updateSessionDetail({ state, detail }) {
33996
34167
  ...state,
33997
34168
  sessionId: detail.id,
33998
34169
  ...detail.sessionNumber === undefined ? {} : { sessionNumber: detail.sessionNumber },
34170
+ ..."title" in detail ? { title: detail.title ?? null } : {},
33999
34171
  aggregateStatus: detail.status,
34172
+ ...detail.agentStatus === undefined ? {} : { agentStatus: detail.agentStatus },
34000
34173
  pullRequest: toSessionPullRequest(detail),
34001
34174
  connectionPreviews: toConnectionPreviews(detail)
34002
34175
  };
@@ -34038,7 +34211,7 @@ function sessionTurnOutcome({ state, sessionMessageId }) {
34038
34211
  }
34039
34212
  function projectEphemeralEvent({ state, event }) {
34040
34213
  const messageDelta = agentMessageDeltaEventSchema.safeParse(event);
34041
- if (messageDelta.success && messageDelta.data.payload.role === "assistant") {
34214
+ if (messageDelta.success && messageDelta.data.actor.type === "main" && messageDelta.data.payload.role === "assistant") {
34042
34215
  return {
34043
34216
  ...state,
34044
34217
  previews: { ...state.previews, assistantText: state.previews.assistantText + messageDelta.data.payload.delta }
@@ -34059,6 +34232,7 @@ function projectRetainedEvent({
34059
34232
  const workspaceGitRevision = sessionWorkspaceGitRevisionEventSchema.safeParse(event);
34060
34233
  const turnEnded = agentTurnEndedEventSchema.safeParse(event);
34061
34234
  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 });
34235
+ projected = recordAgentLineageEvent({ state: projected, event, retainedEventId });
34062
34236
  if (artifact) {
34063
34237
  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 };
34064
34238
  const timelineArtifact = artifact.kind === "file" ? {
@@ -34067,7 +34241,9 @@ function projectRetainedEvent({
34067
34241
  artifactId: artifact.artifact_id,
34068
34242
  occurredAt,
34069
34243
  title: artifact.filename,
34070
- mediaType: artifact.media_type
34244
+ mediaType: artifact.media_type,
34245
+ ...artifact.download_url === undefined ? {} : { downloadUrl: artifact.download_url },
34246
+ ...artifact.preview_url === undefined ? {} : { previewUrl: artifact.preview_url }
34071
34247
  } : {
34072
34248
  kind: "artifact",
34073
34249
  artifactKind: artifact.kind,
@@ -34171,19 +34347,22 @@ function projectSessionWorkspaceGitRevision({ state, event, occurredAt, retained
34171
34347
  });
34172
34348
  }
34173
34349
  function projectAgentTurnEnded({ state, event, occurredAt, retainedEventId }) {
34174
- const retainedTurnEnds = { ...state.retainedTurnEnds, [event.turnId]: { actorType: event.actor.type, outcome: event.payload.status } };
34175
- const messageTurns = event.actor.type === "main" ? Object.fromEntries(Object.entries(state.messageTurns).map(([messageId, turn]) => [messageId, turn.turnId === event.turnId ? { ...turn, outcome: event.payload.status } : turn])) : state.messageTurns;
34176
- return appendActivity({
34177
- state: { ...state, retainedTurnEnds, messageTurns },
34178
- retainedEventId,
34179
- occurredAt,
34180
- card: event.payload.status === "failed" ? { kind: "failure", weight: "signal", title: "Remy turn failed", summary: "Turn failed." } : { kind: "lifecycle", weight: "noise", title: "Remy turn ended", summary: `Turn ${event.payload.status}.` }
34350
+ const stateWithTurnOutcome = event.actor.type === "main" ? {
34351
+ ...state,
34352
+ retainedTurnEnds: { ...state.retainedTurnEnds, [event.turnId]: { actorType: event.actor.type, outcome: event.payload.status } },
34353
+ messageTurns: Object.fromEntries(Object.entries(state.messageTurns).map(([messageId, turn]) => [messageId, turn.turnId === event.turnId ? { ...turn, outcome: event.payload.status } : turn]))
34354
+ } : state;
34355
+ const card = event.actor.type === "main" ? event.payload.status === "failed" ? { kind: "failure", weight: "signal", title: "Remy turn failed", summary: "Turn failed." } : { kind: "lifecycle", weight: "noise", title: "Remy turn ended", summary: `Turn ${event.payload.status}.` } : createChildActivityCard({
34356
+ identity: event.actor,
34357
+ activityTitle: event.payload.status === "failed" ? "Turn failed" : "Turn ended",
34358
+ card: event.payload.status === "failed" ? { kind: "failure", weight: "signal", summary: providerText(event.payload.error?.message, "Turn failed.") } : { kind: "lifecycle", weight: "noise", summary: `Turn ${event.payload.status}.` }
34181
34359
  });
34360
+ return appendActivity({ state: stateWithTurnOutcome, retainedEventId, occurredAt, card });
34182
34361
  }
34183
34362
  function projectDurableAgentEvent({ state, event, occurredAt, retainedEventId }) {
34184
34363
  const agentEvent = parseDurableAgentEvent(event);
34185
34364
  if (agentEvent.type === "agent.message.ended")
34186
- return projectAgentMessageEnded({ state, event: agentEvent, occurredAt });
34365
+ return projectAgentMessageEnded({ state, event: agentEvent, occurredAt, retainedEventId });
34187
34366
  if (agentEvent.type === "agent.context.updated")
34188
34367
  return state;
34189
34368
  if (agentEvent.type === "agent.work.observed" && agentEvent.actor.type === "main")
@@ -34195,6 +34374,9 @@ function projectDurableAgentEvent({ state, event, occurredAt, retainedEventId })
34195
34374
  } : state;
34196
34375
  return appendActivity({ state: stateWithTurnStart, retainedEventId, occurredAt, card: toAgentActivityCard(agentEvent) });
34197
34376
  }
34377
+ function hasOwnContextIdentity(identities, identity) {
34378
+ return Object.prototype.hasOwnProperty.call(identities, identity);
34379
+ }
34198
34380
  function projectAgentWorkObserved({
34199
34381
  state,
34200
34382
  event,
@@ -34266,23 +34448,45 @@ function patchedWorkItemId(observation) {
34266
34448
  return observation.itemId;
34267
34449
  return null;
34268
34450
  }
34269
- function projectAgentMessageEnded({ state, event, occurredAt }) {
34451
+ function projectAgentMessageEnded({ state, event, occurredAt, retainedEventId }) {
34270
34452
  const messageId = event.payload.messageId;
34453
+ const text = event.payload.content.map(publicMessageContentSegmentText).join("");
34454
+ if (event.actor.type === "subagent") {
34455
+ const childId = terminalSafeSingleLine(event.actor.subagentId);
34456
+ const card = createChildActivityCard({
34457
+ identity: event.actor,
34458
+ activityTitle: event.payload.role === "assistant" ? "Message completed" : `${capitalize(event.payload.role)} message`,
34459
+ titleIncludesActivity: false,
34460
+ card: {
34461
+ kind: event.payload.role === "assistant" ? "command-output" : "progress",
34462
+ weight: "signal",
34463
+ summary: `${event.payload.role === "assistant" ? "Message completed." : `${capitalize(event.payload.role)} message retained.`} \xB7 ${childId}`,
34464
+ detail: stripAnsi(text) || "[Empty message]"
34465
+ }
34466
+ });
34467
+ return appendActivity({ state, retainedEventId, occurredAt, card });
34468
+ }
34271
34469
  if (state.transcript.some((item) => item.kind === "message" && item.messageId === messageId))
34272
34470
  return state;
34273
34471
  const role2 = event.payload.role;
34274
34472
  const author = role2 === "assistant" ? { label: "Remy", role: "remy" } : role2 === "system" ? { label: "System", role: "system" } : { label: event.payload.commandId ? state.commandAuthorLabels[event.payload.commandId] ?? "Human" : "Human", role: "human" };
34275
- const text = event.payload.content.map(publicMessageContentSegmentText).join("") || (author.role === "remy" ? "[Remy sent an empty message]" : "[Empty message]");
34473
+ const displayedText = text || (author.role === "remy" ? "[Remy sent an empty message]" : "[Empty message]");
34276
34474
  return {
34277
34475
  ...state,
34278
34476
  ...author.role === "remy" ? { previews: { ...state.previews, assistantText: "" } } : {},
34279
- transcript: [...state.transcript, { kind: "message", messageId, occurredAt, author, text, attachments: [] }]
34477
+ transcript: [...state.transcript, { kind: "message", messageId, occurredAt, author, text: displayedText, attachments: [] }]
34280
34478
  };
34281
34479
  }
34282
34480
  function appendActivity({ state, retainedEventId, occurredAt, card }) {
34283
34481
  return { ...state, transcript: [...state.transcript, { kind: "activity", activityId: `activity:${retainedEventId}`, occurredAt, card }] };
34284
34482
  }
34285
34483
  function toAgentActivityCard(event) {
34484
+ const identity = childActivityIdentity(event);
34485
+ if (identity)
34486
+ return toChildAgentActivityCard({ event, identity });
34487
+ return toMainAgentActivityCard(event);
34488
+ }
34489
+ function toMainAgentActivityCard(event) {
34286
34490
  switch (event.type) {
34287
34491
  case "agent.session.started":
34288
34492
  return { kind: "lifecycle", weight: "noise", title: "Remy session started", summary: "Session is ready." };
@@ -34347,6 +34551,444 @@ function toAgentActivityCard(event) {
34347
34551
  throw new SessionProjectionProtocolError("Cannot project an unknown retained agent event.");
34348
34552
  }
34349
34553
  }
34554
+ function toChildAgentActivityCard({ event, identity }) {
34555
+ switch (event.type) {
34556
+ case "agent.turn.started":
34557
+ return createChildActivityCard({ identity, activityTitle: "Turn started", card: { kind: "lifecycle", weight: "noise", summary: "Turn started." } });
34558
+ case "agent.turn.ended":
34559
+ return createChildActivityCard({
34560
+ identity,
34561
+ activityTitle: event.payload.status === "failed" ? "Turn failed" : "Turn ended",
34562
+ card: event.payload.status === "failed" ? { kind: "failure", weight: "signal", summary: providerText(event.payload.error?.message, "Turn failed.") } : { kind: "lifecycle", weight: "noise", summary: `Turn ${event.payload.status}.` }
34563
+ });
34564
+ case "agent.work.observed":
34565
+ return createChildActivityCard({ identity, activityTitle: "Work progress", card: { kind: "progress", weight: "noise", summary: "Subagent updated its work plan.", detail: jsonDetail(event.payload.observations) } });
34566
+ case "agent.message.started":
34567
+ return createChildActivityCard({ identity, activityTitle: "Message started", card: { kind: "lifecycle", weight: "noise", summary: "Subagent is composing a message." } });
34568
+ case "agent.reasoning.started":
34569
+ return createChildActivityCard({ identity, activityTitle: "Reasoning", card: { kind: "reasoning", weight: "noise", summary: "Subagent started a reasoning summary." } });
34570
+ case "agent.reasoning.ended": {
34571
+ const summary = stripAnsi(event.payload.summary ?? "").trim();
34572
+ return createChildActivityCard({
34573
+ identity,
34574
+ activityTitle: "Reasoning",
34575
+ card: summary.length > 0 ? { kind: "reasoning", weight: "signal", summary } : { kind: "reasoning", weight: "noise", summary: "Subagent completed a reasoning summary." }
34576
+ });
34577
+ }
34578
+ case "agent.tool.call.started":
34579
+ return createChildActivityCard({
34580
+ identity,
34581
+ activityTitle: terminalSafeSingleLine(event.payload.toolName),
34582
+ card: {
34583
+ kind: "tool",
34584
+ weight: "signal",
34585
+ summary: "Tool started.",
34586
+ ...event.payload.input === undefined ? {} : { detail: jsonDetail(event.payload.input), detailFormat: "code" }
34587
+ }
34588
+ });
34589
+ case "agent.tool.call.completed": {
34590
+ const failed = event.payload.status === "failed" || event.payload.status === "cancelled";
34591
+ const detail = event.payload.output === undefined ? undefined : toolOutputDetail(event.payload.output);
34592
+ return createChildActivityCard({
34593
+ identity,
34594
+ activityTitle: failed ? "Tool failed" : "Tool completed",
34595
+ card: failed ? { kind: "failure", weight: "signal", summary: providerText(event.payload.error?.message, `Tool ${event.payload.status}.`), ...detail === undefined ? {} : { detail, detailFormat: "code" } } : { kind: "command-output", weight: "signal", summary: "Tool completed.", ...detail === undefined ? {} : { detail, detailFormat: "code" } }
34596
+ });
34597
+ }
34598
+ case "agent.subagent.started":
34599
+ return createChildActivityCard({ identity, activityTitle: "Started", card: { kind: "progress", weight: "signal", summary: providerText(event.payload.name, event.payload.subagentId) } });
34600
+ case "agent.subagent.progress":
34601
+ return createChildActivityCard({ identity, activityTitle: "Progress", card: { kind: "progress", weight: "signal", summary: providerText(event.payload.summary, "Subagent is working.") } });
34602
+ case "agent.subagent.ended":
34603
+ return createChildActivityCard({
34604
+ identity,
34605
+ activityTitle: event.payload.status === "failed" ? "Failed" : "Ended",
34606
+ card: event.payload.status === "failed" ? { kind: "failure", weight: "signal", summary: providerText(event.payload.error?.message, "Subagent failed.") } : { kind: "progress", weight: "signal", summary: providerText(event.payload.summary, `Subagent ${event.payload.status}.`) }
34607
+ });
34608
+ case "agent.usage":
34609
+ return createChildActivityCard({ identity, activityTitle: "Usage updated", card: { kind: "progress", weight: "noise", summary: "Subagent reported usage.", detail: jsonDetail(event.payload.usage) } });
34610
+ case "agent.context.compaction.started":
34611
+ return createChildActivityCard({ identity, activityTitle: "Context compaction started", card: { kind: "reasoning", weight: "noise", summary: "Subagent is compacting context." } });
34612
+ case "agent.context.compaction.completed":
34613
+ return createChildActivityCard({
34614
+ identity,
34615
+ activityTitle: event.payload.status === "failed" ? "Context compaction failed" : "Context compaction completed",
34616
+ card: event.payload.status === "failed" ? { kind: "failure", weight: "signal", summary: providerText(event.payload.error?.message, "Context compaction failed.") } : { kind: "reasoning", weight: "noise", summary: "Subagent compacted context." }
34617
+ });
34618
+ case "agent.user-input.requested":
34619
+ return createChildActivityCard({ identity, activityTitle: "Needs input", card: { kind: "approval-question", weight: "signal", summary: providerText(event.payload.prompt, "Subagent needs input."), detail: jsonDetail(event.payload.questions) } });
34620
+ case "agent.session.started":
34621
+ case "agent.session.configured":
34622
+ case "agent.session.skills.updated":
34623
+ case "agent.session.state.changed":
34624
+ case "agent.session.ended":
34625
+ case "agent.user-input.resolved":
34626
+ case "agent.error":
34627
+ throw new SessionProjectionProtocolError(`Cannot attribute non-child session event type ${event.type} to a subagent.`);
34628
+ case "agent.message.delta":
34629
+ case "agent.reasoning.summary.delta":
34630
+ case "agent.tool.output.delta":
34631
+ throw new SessionProjectionProtocolError(`Cannot project ephemeral session event type ${event.type} as retained history.`);
34632
+ default:
34633
+ throw new SessionProjectionProtocolError("Cannot attribute an unknown retained event to a subagent.");
34634
+ }
34635
+ }
34636
+ function createChildActivityCard({
34637
+ identity,
34638
+ activityTitle,
34639
+ titleIncludesActivity = true,
34640
+ card
34641
+ }) {
34642
+ const subagentId = terminalSafeSingleLine(identity.subagentId);
34643
+ const actorId = terminalSafeSingleLine(identity.actorId);
34644
+ const displayName = terminalSafeSingleLine(identity.name ?? "") || subagentId;
34645
+ const identityDetail = [
34646
+ `Subagent ID: ${subagentId}`,
34647
+ ...actorId !== subagentId ? [`Actor ID: ${actorId}`] : []
34648
+ ].join(`
34649
+ `);
34650
+ const detail = card.detail === undefined ? identityDetail : `${identityDetail}
34651
+
34652
+ ${card.detail}`;
34653
+ return {
34654
+ ...card,
34655
+ title: `Subagent \xB7 ${displayName}${activityTitle === undefined || !titleIncludesActivity ? "" : ` \xB7 ${activityTitle}`}`,
34656
+ detail,
34657
+ attribution: {
34658
+ status: "unresolved",
34659
+ identity,
34660
+ ...activityTitle === undefined ? {} : { activityTitle }
34661
+ }
34662
+ };
34663
+ }
34664
+ function childActivityIdentity(event) {
34665
+ if (event.type === "agent.subagent.started" || event.type === "agent.subagent.progress" || event.type === "agent.subagent.ended") {
34666
+ if (event.actor.type === "subagent") {
34667
+ if (!subagentLifecycleIdentityMatches({ actor: event.actor, payload: event.payload }))
34668
+ throw new SessionProjectionProtocolError(`Retained session event type ${event.type} carried contradictory subagent actor and payload identity.`);
34669
+ return {
34670
+ ...event.actor,
34671
+ ...event.actor.name === undefined && event.payload.name !== undefined ? { name: event.payload.name } : {}
34672
+ };
34673
+ }
34674
+ return {
34675
+ type: "subagent",
34676
+ actorId: event.payload.actorId,
34677
+ subagentId: event.payload.subagentId,
34678
+ parentActorId: event.payload.parentActorId,
34679
+ origin: event.payload.origin,
34680
+ ...event.payload.parentToolCallId === undefined ? {} : { parentToolCallId: event.payload.parentToolCallId },
34681
+ ...event.payload.name === undefined ? {} : { name: event.payload.name }
34682
+ };
34683
+ }
34684
+ if ("actor" in event && event.actor.type === "subagent") {
34685
+ return event.actor;
34686
+ }
34687
+ return;
34688
+ }
34689
+ function subagentLifecycleIdentityMatches({ actor, payload }) {
34690
+ return actor.actorId === payload.actorId && actor.subagentId === payload.subagentId && actor.parentActorId === payload.parentActorId && actor.parentToolCallId === payload.parentToolCallId && subagentOriginsMatch(actor.origin, payload.origin);
34691
+ }
34692
+ function subagentOriginsMatch(left, right) {
34693
+ if (left.type === "tool_call")
34694
+ return right.type === "tool_call" && left.toolCallId === right.toolCallId;
34695
+ return right.type === "provider_task" && left.taskId === right.taskId;
34696
+ }
34697
+ function recordAgentLineageEvent({
34698
+ state,
34699
+ event,
34700
+ retainedEventId
34701
+ }) {
34702
+ if (!event.type.startsWith("agent."))
34703
+ return state;
34704
+ let parsed;
34705
+ try {
34706
+ parsed = parseDurableAgentEvent(event);
34707
+ } catch (error93) {
34708
+ if (error93 instanceof SessionProjectionProtocolError)
34709
+ return state;
34710
+ throw error93;
34711
+ }
34712
+ const runtimeBase = lineageRuntimeBase(parsed);
34713
+ let childLineage = state.childLineage;
34714
+ if (parsed.type === "agent.session.started" || parsed.type === "agent.session.ended") {
34715
+ const boundaryKey = `${runtimeBase}:${lengthPrefixed(parsed.eventId)}`;
34716
+ if (hasOwnContextIdentity(childLineage.seenRuntimeBoundaryEventIds, boundaryKey))
34717
+ return state;
34718
+ childLineage = {
34719
+ ...childLineage,
34720
+ runtimeEpochs: parsed.type === "agent.session.started" ? { ...childLineage.runtimeEpochs, [runtimeBase]: (childLineage.runtimeEpochs[runtimeBase] ?? 0) + 1 } : childLineage.runtimeEpochs,
34721
+ seenRuntimeBoundaryEventIds: {
34722
+ ...childLineage.seenRuntimeBoundaryEventIds,
34723
+ [boundaryKey]: true
34724
+ }
34725
+ };
34726
+ return { ...state, childLineage };
34727
+ }
34728
+ if (!("turnId" in parsed))
34729
+ return state;
34730
+ const runtimeScope = lineageRuntimeScope({
34731
+ runtimeBase,
34732
+ epoch: childLineage.runtimeEpochs[runtimeBase] ?? 0
34733
+ });
34734
+ const order = childLineage.nextOrder;
34735
+ const identity = childActivityIdentity(parsed);
34736
+ const fact = identity ? {
34737
+ retainedEventId,
34738
+ order,
34739
+ runtimeScope,
34740
+ turnId: parsed.turnId,
34741
+ eventType: parsed.type,
34742
+ identity
34743
+ } : parsed.type === "agent.tool.call.started" && parsed.actor.type === "main" ? {
34744
+ retainedEventId,
34745
+ order,
34746
+ runtimeScope,
34747
+ turnId: parsed.turnId,
34748
+ eventType: parsed.type,
34749
+ toolCallId: parsed.payload.toolCallId
34750
+ } : undefined;
34751
+ if (!fact)
34752
+ return state;
34753
+ const nextState = {
34754
+ ...state,
34755
+ childLineage: {
34756
+ ...childLineage,
34757
+ nextOrder: order + 1,
34758
+ facts: [...childLineage.facts, fact]
34759
+ }
34760
+ };
34761
+ if (!isChildLineageFact(fact) || isSubagentLifecycleType(fact.eventType))
34762
+ return reconcileChildLineage(nextState);
34763
+ const resolution = resolveBodyFactAgainstOwners(childLineage.owners, fact);
34764
+ if (resolution.status === "graph-change")
34765
+ return reconcileChildLineage(nextState);
34766
+ if (resolution.status === "unresolved")
34767
+ return nextState;
34768
+ return stampResolvedAttribution({ state: nextState, retainedEventId, fact, owner: resolution.owner });
34769
+ }
34770
+ function resolveBodyFactAgainstOwners(owners, fact) {
34771
+ const candidates = owners.filter((owner) => owner.runtimeScope === fact.runtimeScope && childIdentityMatches(owner.identity, fact.identity));
34772
+ const turnCandidates = candidates.filter((owner) => owner.childTurnIds.has(fact.turnId));
34773
+ if (turnCandidates.length === 1)
34774
+ return { status: "known", owner: turnCandidates[0] };
34775
+ if (candidates.length === 1)
34776
+ return { status: "graph-change" };
34777
+ return { status: "unresolved" };
34778
+ }
34779
+ function stampResolvedAttribution({ state, retainedEventId, fact, owner }) {
34780
+ const activityId = `activity:${retainedEventId}`;
34781
+ for (let index = state.transcript.length - 1;index >= 0; index -= 1) {
34782
+ const item = state.transcript[index];
34783
+ if (item.kind !== "activity" || item.activityId !== activityId)
34784
+ continue;
34785
+ if (!item.card.attribution)
34786
+ return state;
34787
+ const attribution = {
34788
+ status: "resolved",
34789
+ identity: fact.identity,
34790
+ ...item.card.attribution.activityTitle === undefined ? {} : { activityTitle: item.card.attribution.activityTitle },
34791
+ ownerKey: owner.key,
34792
+ owningMainTurnId: owner.owningMainTurnId,
34793
+ path: ownerPath(owner)
34794
+ };
34795
+ if (childAttributionEqual(item.card.attribution, attribution))
34796
+ return state;
34797
+ const transcript = [...state.transcript];
34798
+ transcript[index] = { ...item, card: { ...item.card, attribution } };
34799
+ return { ...state, transcript };
34800
+ }
34801
+ return state;
34802
+ }
34803
+ function lineageRuntimeBase(event) {
34804
+ return `${lengthPrefixed(event.sessionId)}:${lengthPrefixed(event.providerSessionId)}`;
34805
+ }
34806
+ function lineageRuntimeScope({ runtimeBase, epoch }) {
34807
+ return `${runtimeBase}:${epoch}`;
34808
+ }
34809
+ function lengthPrefixed(value) {
34810
+ return `${value.length}:${value}`;
34811
+ }
34812
+ function isChildLineageFact(fact) {
34813
+ return "identity" in fact;
34814
+ }
34815
+ function isSubagentLifecycleType(type) {
34816
+ return type === "agent.subagent.started" || type === "agent.subagent.progress" || type === "agent.subagent.ended";
34817
+ }
34818
+ function childIdentityMatches(left, right) {
34819
+ return left.actorId === right.actorId && left.subagentId === right.subagentId && left.parentActorId === right.parentActorId && left.parentToolCallId === right.parentToolCallId && subagentOriginsMatch(left.origin, right.origin);
34820
+ }
34821
+ function childIdentityCanAnchor(identity) {
34822
+ return identity.actorId !== identity.parentActorId && (identity.origin.type !== "tool_call" || identity.parentToolCallId === undefined || identity.parentToolCallId === identity.origin.toolCallId);
34823
+ }
34824
+ function reconcileChildLineage(state) {
34825
+ const facts = state.childLineage.facts;
34826
+ const childFacts = facts.filter(isChildLineageFact);
34827
+ const mainToolTurns = new Map;
34828
+ for (const fact of facts) {
34829
+ if (isChildLineageFact(fact))
34830
+ continue;
34831
+ const key = `${fact.runtimeScope}:${lengthPrefixed(fact.toolCallId)}`;
34832
+ const turns = mainToolTurns.get(key) ?? new Set;
34833
+ turns.add(fact.turnId);
34834
+ mainToolTurns.set(key, turns);
34835
+ }
34836
+ const owners = [];
34837
+ const ownerByFactOrder = new Map;
34838
+ const mainTurnForToolOrigin = (fact) => {
34839
+ if (fact.identity.origin.type !== "tool_call")
34840
+ return;
34841
+ const turns = mainToolTurns.get(`${fact.runtimeScope}:${lengthPrefixed(fact.identity.origin.toolCallId)}`);
34842
+ return turns?.size === 1 ? [...turns][0] : undefined;
34843
+ };
34844
+ const createOwner = ({
34845
+ fact,
34846
+ owningMainTurnId,
34847
+ parent
34848
+ }) => {
34849
+ if (!childIdentityCanAnchor(fact.identity))
34850
+ return;
34851
+ if (parent && ownerHasActorAncestor(parent, fact.identity.actorId))
34852
+ return;
34853
+ const samePlacement = owners.filter((owner2) => owner2.runtimeScope === fact.runtimeScope && owner2.owningMainTurnId === owningMainTurnId && owner2.parent === parent && owner2.identity.actorId === fact.identity.actorId && owner2.identity.subagentId === fact.identity.subagentId);
34854
+ if (samePlacement.some((owner2) => !childIdentityMatches(owner2.identity, fact.identity)))
34855
+ return;
34856
+ const existing = samePlacement.find((owner2) => childIdentityMatches(owner2.identity, fact.identity));
34857
+ if (existing)
34858
+ return existing;
34859
+ const owner = {
34860
+ key: `${fact.runtimeScope}:${lengthPrefixed(owningMainTurnId)}:${fact.order}`,
34861
+ identity: fact.identity,
34862
+ runtimeScope: fact.runtimeScope,
34863
+ owningMainTurnId,
34864
+ parent,
34865
+ childTurnIds: new Set
34866
+ };
34867
+ owners.push(owner);
34868
+ return owner;
34869
+ };
34870
+ for (const fact of childFacts) {
34871
+ if (fact.eventType !== "agent.subagent.started" || fact.identity.parentActorId !== "main")
34872
+ continue;
34873
+ const toolTurn = mainTurnForToolOrigin(fact);
34874
+ if (fact.identity.origin.type === "tool_call" && toolTurn === undefined) {
34875
+ const turns = mainToolTurns.get(`${fact.runtimeScope}:${lengthPrefixed(fact.identity.origin.toolCallId)}`);
34876
+ if (turns && turns.size > 1)
34877
+ continue;
34878
+ }
34879
+ if (toolTurn !== undefined && toolTurn !== fact.turnId)
34880
+ continue;
34881
+ const owner = createOwner({ fact, owningMainTurnId: fact.turnId, parent: null });
34882
+ if (owner)
34883
+ ownerByFactOrder.set(fact.order, owner);
34884
+ }
34885
+ for (const fact of childFacts) {
34886
+ if (isSubagentLifecycleType(fact.eventType) && fact.eventType !== "agent.subagent.started" && fact.identity.parentActorId === "main") {
34887
+ const toolTurn = mainTurnForToolOrigin(fact);
34888
+ if (toolTurn === undefined)
34889
+ continue;
34890
+ const owner = owners.find((candidate) => candidate.runtimeScope === fact.runtimeScope && candidate.parent === null && candidate.owningMainTurnId === toolTurn && childIdentityMatches(candidate.identity, fact.identity)) ?? createOwner({ fact, owningMainTurnId: toolTurn, parent: null });
34891
+ if (owner) {
34892
+ ownerByFactOrder.set(fact.order, owner);
34893
+ owner.childTurnIds.add(fact.turnId);
34894
+ }
34895
+ }
34896
+ }
34897
+ let changed = true;
34898
+ while (changed) {
34899
+ changed = false;
34900
+ for (const fact of childFacts) {
34901
+ if (ownerByFactOrder.has(fact.order))
34902
+ continue;
34903
+ const candidates = owners.filter((owner2) => owner2.runtimeScope === fact.runtimeScope && childIdentityMatches(owner2.identity, fact.identity));
34904
+ const turnCandidates = candidates.filter((owner2) => owner2.childTurnIds.has(fact.turnId));
34905
+ const owner = turnCandidates.length === 1 ? turnCandidates[0] : candidates.length === 1 ? candidates[0] : undefined;
34906
+ if (!owner || fact.eventType === "agent.subagent.started")
34907
+ continue;
34908
+ ownerByFactOrder.set(fact.order, owner);
34909
+ owner.childTurnIds.add(fact.turnId);
34910
+ changed = true;
34911
+ }
34912
+ for (const fact of childFacts) {
34913
+ if (ownerByFactOrder.has(fact.order) || fact.eventType !== "agent.subagent.started" || fact.identity.parentActorId === "main" || !childIdentityCanAnchor(fact.identity)) {
34914
+ continue;
34915
+ }
34916
+ const parentCandidates = owners.filter((owner2) => owner2.runtimeScope === fact.runtimeScope && owner2.identity.actorId === fact.identity.parentActorId);
34917
+ const turnParents = parentCandidates.filter((owner2) => owner2.childTurnIds.has(fact.turnId));
34918
+ const parent = turnParents.length === 1 ? turnParents[0] : parentCandidates.length === 1 ? parentCandidates[0] : undefined;
34919
+ if (!parent)
34920
+ continue;
34921
+ const owner = createOwner({ fact, owningMainTurnId: parent.owningMainTurnId, parent });
34922
+ if (!owner)
34923
+ continue;
34924
+ ownerByFactOrder.set(fact.order, owner);
34925
+ changed = true;
34926
+ }
34927
+ }
34928
+ const resolvedByRetainedEventId = new Map;
34929
+ for (const fact of childFacts) {
34930
+ const owner = ownerByFactOrder.get(fact.order);
34931
+ if (owner)
34932
+ resolvedByRetainedEventId.set(fact.retainedEventId, { fact, owner });
34933
+ }
34934
+ let transcriptChanged = false;
34935
+ const transcript = state.transcript.map((item) => {
34936
+ if (item.kind !== "activity" || !item.card.attribution)
34937
+ return item;
34938
+ const retainedEventId = item.activityId.slice("activity:".length);
34939
+ const resolved = resolvedByRetainedEventId.get(retainedEventId);
34940
+ const attribution = resolved ? {
34941
+ status: "resolved",
34942
+ identity: resolved.fact.identity,
34943
+ ...item.card.attribution.activityTitle === undefined ? {} : { activityTitle: item.card.attribution.activityTitle },
34944
+ ownerKey: resolved.owner.key,
34945
+ owningMainTurnId: resolved.owner.owningMainTurnId,
34946
+ path: ownerPath(resolved.owner)
34947
+ } : {
34948
+ status: "unresolved",
34949
+ identity: item.card.attribution.identity,
34950
+ ...item.card.attribution.activityTitle === undefined ? {} : { activityTitle: item.card.attribution.activityTitle }
34951
+ };
34952
+ if (childAttributionEqual(item.card.attribution, attribution))
34953
+ return item;
34954
+ transcriptChanged = true;
34955
+ return { ...item, card: { ...item.card, attribution } };
34956
+ });
34957
+ const nextState = { ...state, childLineage: { ...state.childLineage, owners } };
34958
+ return transcriptChanged ? { ...nextState, transcript } : nextState;
34959
+ }
34960
+ function ownerHasActorAncestor(parent, actorId) {
34961
+ let candidate = parent;
34962
+ while (candidate) {
34963
+ if (candidate.identity.actorId === actorId)
34964
+ return true;
34965
+ candidate = candidate.parent;
34966
+ }
34967
+ return false;
34968
+ }
34969
+ function ownerPath(owner) {
34970
+ const path = [];
34971
+ let candidate = owner;
34972
+ while (candidate) {
34973
+ path.unshift(candidate.identity);
34974
+ candidate = candidate.parent;
34975
+ }
34976
+ return path;
34977
+ }
34978
+ function childAttributionEqual(left, right) {
34979
+ if (left.status !== right.status || left.activityTitle !== right.activityTitle || !childIdentityMatches(left.identity, right.identity)) {
34980
+ return false;
34981
+ }
34982
+ if (left.status === "unresolved" || right.status === "unresolved")
34983
+ return true;
34984
+ return left.ownerKey === right.ownerKey && left.owningMainTurnId === right.owningMainTurnId && left.path.length === right.path.length && left.path.every((identity, index) => childIdentityMatches(identity, right.path[index]));
34985
+ }
34986
+ function terminalSafeSingleLine(value) {
34987
+ return stripAnsi(value).replace(/\s+/gu, " ").trim();
34988
+ }
34989
+ function capitalize(value) {
34990
+ return value.length === 0 ? value : `${value[0].toUpperCase()}${value.slice(1)}`;
34991
+ }
34350
34992
  function parseDurableAgentEvent(event) {
34351
34993
  if (!event.type.startsWith("agent."))
34352
34994
  throw new SessionProjectionProtocolError(`Cannot project retained session event type ${event.type}.`);
@@ -34383,6 +35025,9 @@ var terminalControlPattern = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g;
34383
35025
  function stripAnsi(value) {
34384
35026
  return value.replace(ansiEscapePattern, "").replace(terminalControlPattern, "");
34385
35027
  }
35028
+ function terminalSafeText(value) {
35029
+ return stripAnsi(value);
35030
+ }
34386
35031
  function formatReasoningPreview(item) {
34387
35032
  if (item.status === "absent")
34388
35033
  return "";
@@ -34583,12 +35228,12 @@ function createRemoteSessionController(dependencies) {
34583
35228
  detail: input.detail,
34584
35229
  activeMessageId: dependencies.activeMessageId ?? cache?.activeMessageId
34585
35230
  });
34586
- resolveReady();
34587
- publishState();
34588
35231
  if (input.mode === "cold-resume") {
34589
35232
  const hydrationGeneration = beginReasoningAttempt();
34590
35233
  await hydrateRetainedHistoryFromBeginning(hydrationGeneration);
34591
35234
  }
35235
+ resolveReady();
35236
+ publishState();
34592
35237
  await streamWithControllerReconnects();
34593
35238
  }
34594
35239
  function stop() {
@@ -34624,7 +35269,8 @@ function createRemoteSessionController(dependencies) {
34624
35269
  async function recordActiveMessage(sessionMessageId) {
34625
35270
  state = {
34626
35271
  ...getState(),
34627
- activeMessageId: sessionMessageId
35272
+ activeMessageId: sessionMessageId,
35273
+ agentStatus: undefined
34628
35274
  };
34629
35275
  await writeCache();
34630
35276
  publishState();
@@ -34645,7 +35291,8 @@ function createRemoteSessionController(dependencies) {
34645
35291
  const page = await dependencies.listSessionEvents({
34646
35292
  sessionId: dependencies.sessionId,
34647
35293
  limit: sessionHistoryPageSize,
34648
- after
35294
+ after,
35295
+ signal: abortController.signal
34649
35296
  });
34650
35297
  for (const item of page.data) {
34651
35298
  await reduceFrameAndPersist({
@@ -34899,7 +35546,10 @@ async function sleepMs(ms, signal) {
34899
35546
  }
34900
35547
 
34901
35548
  // src/tui/dashboard.ts
34902
- import { bg, BoxRenderable, bold as bold2, CliRenderEvents as CliRenderEvents2, fg as fg3, ScrollBoxRenderable, stringToStyledText as stringToStyledText2, TextRenderable } from "@opentui/core";
35549
+ import { bg, BoxRenderable as BoxRenderable2, bold as bold2, CliRenderEvents as CliRenderEvents2, fg as fg3, ScrollBoxRenderable, stringToStyledText as stringToStyledText2, TextRenderable as TextRenderable2 } from "@opentui/core";
35550
+
35551
+ // src/tui/composer.ts
35552
+ import { BoxRenderable, TextRenderable, TextareaRenderable } from "@opentui/core";
34903
35553
 
34904
35554
  // src/tui/composer-divider.ts
34905
35555
  import { dim as dim2, fg as fg2, StyledText as StyledText2 } from "@opentui/core";
@@ -35094,6 +35744,98 @@ function renderComposerDivider({ width, tag, leadLabel }) {
35094
35744
  ]);
35095
35745
  }
35096
35746
 
35747
+ // src/tui/composer-height.ts
35748
+ var composerMaxRows = 6;
35749
+ function resizeComposer(composer) {
35750
+ composer.height = Math.min(Math.max(composer.editorView.getTotalVirtualLineCount(), 1), composerMaxRows);
35751
+ }
35752
+
35753
+ // src/tui/composer.ts
35754
+ var defaultPlaceholder = "Message Remy\u2026 Tab paths attach \xB7 Enter/Shift+Enter send \xB7 Option+Enter newline";
35755
+ function createComposer({
35756
+ renderer,
35757
+ parent,
35758
+ tag,
35759
+ topDividerLabel,
35760
+ placeholder = defaultPlaceholder,
35761
+ screenPaddingX = 1,
35762
+ onSubmit,
35763
+ onContentChange
35764
+ }) {
35765
+ const root = new BoxRenderable(renderer, { width: "100%", flexDirection: "column", flexShrink: 0 });
35766
+ const dividerTop = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35767
+ const textarea = new TextareaRenderable(renderer, {
35768
+ width: "100%",
35769
+ height: 1,
35770
+ flexShrink: 0,
35771
+ wrapMode: "word",
35772
+ placeholder,
35773
+ keyBindings: [
35774
+ { name: "return", action: "submit" },
35775
+ { name: "return", shift: true, action: "submit" },
35776
+ { name: "return", meta: true, action: "newline" },
35777
+ { name: "j", ctrl: true, action: "newline" }
35778
+ ],
35779
+ onSubmit
35780
+ });
35781
+ const dividerBottom = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35782
+ let mounted = false;
35783
+ root.add(dividerTop);
35784
+ root.add(textarea);
35785
+ root.add(dividerBottom);
35786
+ textarea.onContentChange = () => {
35787
+ resizeComposer(textarea);
35788
+ onContentChange?.();
35789
+ };
35790
+ function render() {
35791
+ const dividerWidth = renderer.width - screenPaddingX * 2;
35792
+ dividerTop.content = renderComposerDivider({ width: dividerWidth, tag, leadLabel: topDividerLabel?.() });
35793
+ dividerBottom.content = renderComposerDivider({ width: dividerWidth });
35794
+ }
35795
+ return {
35796
+ textarea,
35797
+ mount() {
35798
+ if (mounted)
35799
+ return;
35800
+ mounted = true;
35801
+ parent.add(root);
35802
+ resizeComposer(textarea);
35803
+ render();
35804
+ },
35805
+ unmount() {
35806
+ if (!mounted)
35807
+ return;
35808
+ mounted = false;
35809
+ parent.remove(root);
35810
+ },
35811
+ render,
35812
+ focus() {
35813
+ textarea.focus();
35814
+ },
35815
+ destroy() {
35816
+ if (mounted)
35817
+ parent.remove(root);
35818
+ mounted = false;
35819
+ root.destroyRecursively();
35820
+ }
35821
+ };
35822
+ }
35823
+
35824
+ // src/tui/fuzzy-match.ts
35825
+ function fuzzyMatches(value, query) {
35826
+ const normalizedQuery = query.replaceAll(/\s/g, "").toLowerCase();
35827
+ if (!normalizedQuery)
35828
+ return true;
35829
+ let queryIndex = 0;
35830
+ for (const character of value.toLowerCase()) {
35831
+ if (character === normalizedQuery[queryIndex])
35832
+ queryIndex += 1;
35833
+ if (queryIndex === normalizedQuery.length)
35834
+ return true;
35835
+ }
35836
+ return false;
35837
+ }
35838
+
35097
35839
  // src/tui/renderer.ts
35098
35840
  import { CliRenderEvents, createClipboard, createCliRenderer, createHostClipboard, createRendererClipboardAdapter } from "@opentui/core";
35099
35841
  var rendererDestroyed = new WeakMap;
@@ -35176,8 +35918,17 @@ async function renderRemyView({
35176
35918
 
35177
35919
  // src/tui/dashboard.ts
35178
35920
  var DASHBOARD_POLL_INTERVAL_MS = 1e4;
35921
+ var lifecycleStatusOptions = [
35922
+ { value: "open", label: "Open" },
35923
+ { value: "completed", label: "Completed" },
35924
+ { value: "failed", label: "Failed" },
35925
+ { value: "cancelled", label: "Cancelled" }
35926
+ ];
35179
35927
  async function createDashboardTui({
35180
35928
  initialPage,
35929
+ initialFilters = { statuses: [], creators: [] },
35930
+ authors: initialAuthors = [],
35931
+ loadAuthors,
35181
35932
  loadPage,
35182
35933
  createRenderer = createDefaultRenderer
35183
35934
  }) {
@@ -35186,6 +35937,7 @@ async function createDashboardTui({
35186
35937
  let keyHandler;
35187
35938
  let resizeHandler;
35188
35939
  let refreshTimer;
35940
+ let contentScrollHandler;
35189
35941
  let rendererDestroyed2 = false;
35190
35942
  const loadPageAbortController = new AbortController;
35191
35943
  try {
@@ -35197,28 +35949,61 @@ async function createDashboardTui({
35197
35949
  loadPageAbortController.abort();
35198
35950
  }, render = function() {
35199
35951
  const contentWidth = renderer.width - 4;
35200
- const rowsBelow = rowsBelowVisibleWindow({ sessionCount: page.sessions.length, selectedIndex, height: renderer.height });
35952
+ const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
35953
+ const rowsBelow = rowsBelowVisibleWindow({ sessionCount: sessions.length, selectedIndex, height: renderer.height });
35201
35954
  const isNarrow = renderer.width < 72;
35202
- const selectedSession = page.sessions[selectedIndex];
35203
- const sessionRows = page.sessions.length === 0 ? stringToStyledText2("No remote sessions yet.") : isNarrow ? formatSessionCard({
35955
+ const selectedSession = filterMode ? undefined : sessions[selectedIndex];
35956
+ const sessionRows = filterMode ? formatFilterMenu({ mode: filterMode, authors, width: contentWidth }) : sessions.length === 0 ? stringToStyledText2(sessionSearchQuery ? "No sessions match the search." : hasActiveFilters(filters) ? "No sessions match the active filters." : "No remote sessions yet.") : isNarrow ? formatSessionCard({
35204
35957
  session: selectedSession,
35205
35958
  selectedIndex,
35206
- sessionCount: page.sessions.length,
35959
+ sessionCount: sessions.length,
35207
35960
  width: contentWidth
35208
35961
  }) : formatSessionList({
35209
- sessions: page.sessions,
35962
+ sessions,
35210
35963
  selectedIndex,
35211
35964
  width: contentWidth,
35212
35965
  height: renderer.height
35213
35966
  });
35214
- statusBand.content = dashboardStatusBand({ page, pageIndex });
35967
+ statusBand.content = dashboardStatusBand({ page, pageIndex, filters, sessionSearchQuery });
35215
35968
  content.content = joinStyled([stringToStyledText2(""), sessionRows], `
35216
35969
  `);
35970
+ desiredContentScrollY = filterMode ? filterMode.selectedIndex + 2 : 0;
35971
+ if (!contentScrollHandler) {
35972
+ contentScrollHandler = () => {
35973
+ contentScrollHandler = undefined;
35974
+ if (destroyed)
35975
+ return;
35976
+ listRegion.scrollTo({ x: 0, y: desiredContentScrollY });
35977
+ renderer.requestRender();
35978
+ };
35979
+ renderer.once(CliRenderEvents2.FRAME, contentScrollHandler);
35980
+ }
35217
35981
  const showPreview = !isNarrow && selectedSession !== undefined;
35218
35982
  previewBand.visible = showPreview;
35219
35983
  previewBand.content = showPreview ? formatSessionPreview({ session: selectedSession, width: contentWidth }) : stringToStyledText2("");
35220
- separator.content = renderComposerDivider({ width: contentWidth });
35221
- footer.content = dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen });
35984
+ const activeSearchComposer = filterMode?.searching ? "filter" : sessionSearchOpen ? "session" : undefined;
35985
+ if (activeSearchComposer !== mountedSearchComposer) {
35986
+ if (mountedSearchComposer === "filter")
35987
+ filterSearchComposer.unmount();
35988
+ else if (mountedSearchComposer === "session")
35989
+ sessionSearchComposer.unmount();
35990
+ mountedSearchComposer = activeSearchComposer;
35991
+ if (activeSearchComposer === "filter") {
35992
+ filterSearchComposer.mount();
35993
+ filterSearchComposer.focus();
35994
+ } else if (activeSearchComposer === "session") {
35995
+ sessionSearchComposer.mount();
35996
+ sessionSearchComposer.focus();
35997
+ } else {
35998
+ listRegion.focus();
35999
+ }
36000
+ }
36001
+ if (activeSearchComposer === "filter")
36002
+ filterSearchComposer.render();
36003
+ else if (activeSearchComposer === "session")
36004
+ sessionSearchComposer.render();
36005
+ separator.content = activeSearchComposer ? "" : renderComposerDivider({ width: contentWidth });
36006
+ footer.content = filterMode ? dashboardFilterFooter({ mode: filterMode, loadingPage, loadingAuthors, pageError, authorError }) : dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen, sessionSearchOpen });
35222
36007
  renderer.requestRender();
35223
36008
  }, finish = function(value) {
35224
36009
  if (settled)
@@ -35233,9 +36018,40 @@ async function createDashboardTui({
35233
36018
  stopPageRefresh();
35234
36019
  rejectDraft(error93);
35235
36020
  }, moveSelectionTo = function(index) {
35236
- if (page.sessions.length === 0)
36021
+ const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
36022
+ if (sessions.length === 0)
36023
+ return;
36024
+ selectedIndex = Math.max(0, Math.min(index, sessions.length - 1));
36025
+ render();
36026
+ }, applyFilterSearch = function() {
36027
+ if (!filterMode?.searching)
36028
+ return;
36029
+ filterMode = { ...filterMode, searching: false, selectedIndex: 0 };
36030
+ filterSearchComposer.textarea.setText("");
36031
+ render();
36032
+ }, cancelFilterSearch = function() {
36033
+ if (!filterMode?.searching)
36034
+ return;
36035
+ filterMode = { ...filterMode, searching: false, query: "", selectedIndex: 0 };
36036
+ filterSearchComposer.textarea.setText("");
36037
+ render();
36038
+ }, openSessionSearch = function() {
36039
+ sessionSearchOpen = true;
36040
+ sessionSearchComposer.textarea.setText(sessionSearchQuery);
36041
+ render();
36042
+ }, applySessionSearch = function() {
36043
+ if (!sessionSearchOpen)
36044
+ return;
36045
+ sessionSearchOpen = false;
36046
+ selectedIndex = 0;
36047
+ render();
36048
+ }, cancelSessionSearch = function() {
36049
+ if (!sessionSearchOpen)
35237
36050
  return;
35238
- selectedIndex = Math.max(0, Math.min(index, page.sessions.length - 1));
36051
+ sessionSearchOpen = false;
36052
+ sessionSearchQuery = "";
36053
+ selectedIndex = 0;
36054
+ sessionSearchComposer.textarea.setText("");
35239
36055
  render();
35240
36056
  }, handleSessionKey = function(key) {
35241
36057
  if (logoutConfirmationOpen) {
@@ -35248,6 +36064,84 @@ async function createDashboardTui({
35248
36064
  }
35249
36065
  return;
35250
36066
  }
36067
+ if (sessionSearchOpen) {
36068
+ if (key.name === "escape") {
36069
+ key.preventDefault();
36070
+ cancelSessionSearch();
36071
+ }
36072
+ return;
36073
+ }
36074
+ if (filterMode) {
36075
+ const options = visibleDashboardFilterOptions({ mode: filterMode, authors });
36076
+ if (key.name === "escape") {
36077
+ key.preventDefault();
36078
+ if (filterMode.searching) {
36079
+ cancelFilterSearch();
36080
+ return;
36081
+ }
36082
+ filterMode = undefined;
36083
+ pageError = undefined;
36084
+ authorError = undefined;
36085
+ render();
36086
+ return;
36087
+ }
36088
+ if (filterMode.searching)
36089
+ return;
36090
+ if (loadingAuthors) {
36091
+ key.preventDefault();
36092
+ return;
36093
+ }
36094
+ const filterListFocused = renderer.currentFocusedRenderable === listRegion;
36095
+ if (filterListFocused && (key.name === "G" || key.name === "g" && key.shift)) {
36096
+ key.preventDefault();
36097
+ awaitingVimGoToTop = false;
36098
+ if (options.length > 0)
36099
+ filterMode = { ...filterMode, selectedIndex: options.length - 1 };
36100
+ render();
36101
+ return;
36102
+ }
36103
+ if (filterListFocused && key.name === "g") {
36104
+ key.preventDefault();
36105
+ if (awaitingVimGoToTop && options.length > 0)
36106
+ filterMode = { ...filterMode, selectedIndex: 0 };
36107
+ awaitingVimGoToTop = !awaitingVimGoToTop;
36108
+ render();
36109
+ return;
36110
+ }
36111
+ awaitingVimGoToTop = false;
36112
+ if (key.name === "down" || key.name === "up" || key.name === "j" || key.name === "k") {
36113
+ key.preventDefault();
36114
+ if (options.length > 0) {
36115
+ const backwards = key.name === "up" || key.name === "k";
36116
+ filterMode = { ...filterMode, selectedIndex: (filterMode.selectedIndex + (backwards ? -1 : 1) + options.length) % options.length };
36117
+ }
36118
+ render();
36119
+ return;
36120
+ }
36121
+ if (key.name === "space" || key.name === "left" || key.name === "right" || key.name === "h" || key.name === "l") {
36122
+ key.preventDefault();
36123
+ const option = options[filterMode.selectedIndex];
36124
+ if (!option)
36125
+ return;
36126
+ const selected = key.name === "space" ? !isDashboardFilterOptionSelected({ mode: filterMode, option }) : key.name === "right" || key.name === "l";
36127
+ filterMode = updateDashboardFilterSelection({ mode: filterMode, option, selected, authors });
36128
+ render();
36129
+ return;
36130
+ }
36131
+ if (key.name === "s") {
36132
+ key.preventDefault();
36133
+ filterMode = { ...filterMode, query: "", searching: true, selectedIndex: 0 };
36134
+ filterSearchComposer.textarea.setText("");
36135
+ render();
36136
+ return;
36137
+ }
36138
+ if (key.name === "return" || key.name === "enter") {
36139
+ key.preventDefault();
36140
+ const nextFilters = { statuses: [...filterMode.statuses], creators: [...filterMode.creators] };
36141
+ applyFilters(nextFilters);
36142
+ }
36143
+ return;
36144
+ }
35251
36145
  const listFocused = renderer.currentFocusedRenderable === listRegion;
35252
36146
  if (key.name === "down") {
35253
36147
  key.preventDefault();
@@ -35276,7 +36170,7 @@ async function createDashboardTui({
35276
36170
  if (listFocused && (key.name === "G" || key.name === "g" && key.shift)) {
35277
36171
  key.preventDefault();
35278
36172
  awaitingVimGoToTop = false;
35279
- moveSelectionTo(page.sessions.length - 1);
36173
+ moveSelectionTo(visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery }).length - 1);
35280
36174
  return;
35281
36175
  }
35282
36176
  if (listFocused && key.name === "g") {
@@ -35301,7 +36195,7 @@ async function createDashboardTui({
35301
36195
  awaitingVimGoToTop = false;
35302
36196
  if (key.name === "return" || key.name === "enter") {
35303
36197
  key.preventDefault();
35304
- const session = page.sessions[selectedIndex];
36198
+ const session = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery })[selectedIndex];
35305
36199
  if (session)
35306
36200
  finish({ kind: "session", sessionId: session.id });
35307
36201
  return;
@@ -35311,6 +36205,23 @@ async function createDashboardTui({
35311
36205
  finish({ kind: "new" });
35312
36206
  return;
35313
36207
  }
36208
+ if (key.name === "s") {
36209
+ key.preventDefault();
36210
+ openSessionSearch();
36211
+ return;
36212
+ }
36213
+ if (key.name === "f") {
36214
+ key.preventDefault();
36215
+ authorError = undefined;
36216
+ filterMode = createFilterMode({ kind: "status", filters });
36217
+ render();
36218
+ return;
36219
+ }
36220
+ if (key.name === "a") {
36221
+ key.preventDefault();
36222
+ openAuthorFilter();
36223
+ return;
36224
+ }
35314
36225
  if (key.name === "L" || key.shift && key.name === "l") {
35315
36226
  key.preventDefault();
35316
36227
  logoutConfirmationOpen = true;
@@ -35327,21 +36238,19 @@ async function createDashboardTui({
35327
36238
  finish(null);
35328
36239
  }
35329
36240
  };
35330
- const root = new BoxRenderable(renderer, { width: "100%", height: "100%", flexDirection: "column", paddingX: 2, overflow: "hidden" });
35331
- const statusBand = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35332
- const content = new TextRenderable(renderer, { content: "" });
36241
+ const root = new BoxRenderable2(renderer, { width: "100%", height: "100%", flexDirection: "column", paddingX: 2, overflow: "hidden" });
36242
+ const statusBand = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
36243
+ const content = new TextRenderable2(renderer, { content: "" });
35333
36244
  const listRegion = new ScrollBoxRenderable(renderer, { flexGrow: 1, flexShrink: 1, minHeight: 0, scrollY: true });
35334
- const previewBand = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35335
- const separator = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35336
- const footer = new TextRenderable(renderer, { content: "", flexShrink: 0 });
35337
- root.onMouseDown = (event) => {
35338
- event.preventDefault();
35339
- listRegion.focus();
35340
- };
36245
+ const previewBand = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
36246
+ const filterSearchRegion = new BoxRenderable2(renderer, { width: "100%", flexShrink: 0 });
36247
+ const separator = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
36248
+ const footer = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
35341
36249
  listRegion.add(content);
35342
36250
  root.add(statusBand);
35343
36251
  root.add(listRegion);
35344
36252
  root.add(previewBand);
36253
+ root.add(filterSearchRegion);
35345
36254
  root.add(separator);
35346
36255
  root.add(footer);
35347
36256
  renderer.root.add(root);
@@ -35351,8 +36260,22 @@ async function createDashboardTui({
35351
36260
  let loadingPage = false;
35352
36261
  let pageRequestInFlight = false;
35353
36262
  let pageError;
36263
+ let authorError;
36264
+ let authors = initialAuthors;
36265
+ let authorsLoaded = loadAuthors === undefined || initialAuthors.length > 0;
36266
+ let loadingAuthors = false;
35354
36267
  let awaitingVimGoToTop = false;
35355
36268
  let logoutConfirmationOpen = false;
36269
+ let filters = initialFilters;
36270
+ let filterMode;
36271
+ let sessionSearchOpen = false;
36272
+ let sessionSearchQuery = "";
36273
+ root.onMouseDown = (event) => {
36274
+ if (filterMode?.searching || sessionSearchOpen)
36275
+ return;
36276
+ event.preventDefault();
36277
+ listRegion.focus();
36278
+ };
35356
36279
  let destroyed = false;
35357
36280
  let rendererDestroyPromise;
35358
36281
  let settled = false;
@@ -35362,10 +36285,41 @@ async function createDashboardTui({
35362
36285
  let rejectDraft = () => {
35363
36286
  return;
35364
36287
  };
36288
+ let desiredContentScrollY = 0;
35365
36289
  const action = new Promise((resolve, reject) => {
35366
36290
  resolveAction = resolve;
35367
36291
  rejectDraft = reject;
35368
36292
  });
36293
+ let mountedSearchComposer;
36294
+ const filterSearchComposer = createComposer({
36295
+ renderer,
36296
+ parent: filterSearchRegion,
36297
+ tag: "Filter",
36298
+ placeholder: "Filter options\u2026",
36299
+ screenPaddingX: 2,
36300
+ onSubmit: () => applyFilterSearch(),
36301
+ onContentChange: () => {
36302
+ if (!filterMode?.searching)
36303
+ return;
36304
+ filterMode = { ...filterMode, query: filterSearchComposer.textarea.plainText, selectedIndex: 0 };
36305
+ render();
36306
+ }
36307
+ });
36308
+ const sessionSearchComposer = createComposer({
36309
+ renderer,
36310
+ parent: filterSearchRegion,
36311
+ tag: "Search",
36312
+ placeholder: "Search by ID, title, or number\u2026",
36313
+ screenPaddingX: 2,
36314
+ onSubmit: () => applySessionSearch(),
36315
+ onContentChange: () => {
36316
+ if (!sessionSearchOpen)
36317
+ return;
36318
+ sessionSearchQuery = sessionSearchComposer.textarea.plainText;
36319
+ selectedIndex = 0;
36320
+ render();
36321
+ }
36322
+ });
35369
36323
  async function loadAdjacentPage(direction) {
35370
36324
  if (pageRequestInFlight)
35371
36325
  return;
@@ -35382,7 +36336,8 @@ async function createDashboardTui({
35382
36336
  return;
35383
36337
  page = loadedPage;
35384
36338
  pageIndex = Math.max(1, pageIndex + (direction === "next" ? 1 : -1));
35385
- selectedIndex = direction === "next" ? 0 : Math.max(0, page.sessions.length - 1);
36339
+ const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
36340
+ selectedIndex = direction === "next" ? 0 : Math.max(0, sessions.length - 1);
35386
36341
  } catch (error93) {
35387
36342
  if (!destroyed && !settled)
35388
36343
  pageError = error93 instanceof Error ? error93.message : String(error93);
@@ -35401,7 +36356,7 @@ async function createDashboardTui({
35401
36356
  const refreshedPage = await loadPage({ target: "current", signal: loadPageAbortController.signal });
35402
36357
  if (destroyed || settled)
35403
36358
  return;
35404
- const selectedSessionId = page.sessions[selectedIndex]?.id;
36359
+ const selectedSessionId = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery })[selectedIndex]?.id;
35405
36360
  page = refreshedPage;
35406
36361
  const refreshedSelectedIndex = selectedSessionId ? page.sessions.findIndex((session) => session.id === selectedSessionId) : -1;
35407
36362
  selectedIndex = refreshedSelectedIndex >= 0 ? refreshedSelectedIndex : Math.max(0, Math.min(selectedIndex, page.sessions.length - 1));
@@ -35416,10 +36371,53 @@ async function createDashboardTui({
35416
36371
  pageRequestInFlight = false;
35417
36372
  }
35418
36373
  }
36374
+ async function applyFilters(nextFilters) {
36375
+ if (loadingPage)
36376
+ return;
36377
+ loadingPage = true;
36378
+ pageError = undefined;
36379
+ authorError = undefined;
36380
+ render();
36381
+ try {
36382
+ page = await loadPage({ filters: nextFilters, signal: loadPageAbortController.signal });
36383
+ filters = nextFilters;
36384
+ pageIndex = 1;
36385
+ selectedIndex = 0;
36386
+ filterMode = undefined;
36387
+ } catch (error93) {
36388
+ pageError = error93 instanceof Error ? error93.message : String(error93);
36389
+ } finally {
36390
+ loadingPage = false;
36391
+ render();
36392
+ }
36393
+ }
36394
+ async function openAuthorFilter() {
36395
+ filterMode = createFilterMode({ kind: "creator", filters });
36396
+ authorError = undefined;
36397
+ render();
36398
+ if (authorsLoaded || loadingAuthors || !loadAuthors)
36399
+ return;
36400
+ loadingAuthors = true;
36401
+ render();
36402
+ try {
36403
+ authors = await loadAuthors();
36404
+ authorsLoaded = true;
36405
+ if (filterMode?.kind === "creator") {
36406
+ const firstSelectedIndex = authors.findIndex((author) => filterMode?.creators.some((creator) => creator.id === author.id));
36407
+ filterMode = { ...filterMode, selectedIndex: Math.max(0, firstSelectedIndex) };
36408
+ }
36409
+ } catch (error93) {
36410
+ authorError = error93 instanceof Error ? error93.message : String(error93);
36411
+ } finally {
36412
+ loadingAuthors = false;
36413
+ render();
36414
+ }
36415
+ }
35419
36416
  async function moveSelection(direction) {
35420
- if (loadingPage || page.sessions.length === 0)
36417
+ const sessions = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery });
36418
+ if (loadingPage || sessions.length === 0)
35421
36419
  return;
35422
- const isBoundary = direction === "next" ? selectedIndex === page.sessions.length - 1 : selectedIndex === 0;
36420
+ const isBoundary = direction === "next" ? selectedIndex === sessions.length - 1 : selectedIndex === 0;
35423
36421
  if (!isBoundary) {
35424
36422
  selectedIndex += direction === "next" ? 1 : -1;
35425
36423
  render();
@@ -35434,7 +36432,13 @@ async function createDashboardTui({
35434
36432
  }
35435
36433
  return false;
35436
36434
  };
35437
- keyHandler = handleSessionKey;
36435
+ filterSearchComposer.textarea.onKeyDown = handleSessionKey;
36436
+ sessionSearchComposer.textarea.onKeyDown = handleSessionKey;
36437
+ keyHandler = (key) => {
36438
+ if (mountedSearchComposer && key.name !== "escape" && !(key.ctrl && key.name === "c"))
36439
+ return;
36440
+ handleSessionKey(key);
36441
+ };
35438
36442
  renderer.addInputHandler(inputHandler);
35439
36443
  renderer.keyInput.on("keypress", keyHandler);
35440
36444
  renderer.on(CliRenderEvents2.RENDER_ERROR, (event) => fail(event.error));
@@ -35461,6 +36465,10 @@ async function createDashboardTui({
35461
36465
  if (resizeHandler)
35462
36466
  renderer.off(CliRenderEvents2.RESIZE, resizeHandler);
35463
36467
  stopPageRefresh();
36468
+ if (contentScrollHandler)
36469
+ renderer.off(CliRenderEvents2.FRAME, contentScrollHandler);
36470
+ filterSearchComposer.destroy();
36471
+ sessionSearchComposer.destroy();
35464
36472
  rendererDestroyed2 = true;
35465
36473
  renderer.destroy();
35466
36474
  },
@@ -35478,6 +36486,8 @@ async function createDashboardTui({
35478
36486
  if (refreshTimer !== undefined)
35479
36487
  clearInterval(refreshTimer);
35480
36488
  loadPageAbortController.abort();
36489
+ if (contentScrollHandler)
36490
+ renderer.off(CliRenderEvents2.FRAME, contentScrollHandler);
35481
36491
  if (!rendererDestroyed2) {
35482
36492
  rendererDestroyed2 = true;
35483
36493
  renderer.destroy();
@@ -35515,6 +36525,63 @@ function rowsBelowVisibleWindow({ sessionCount, selectedIndex, height }) {
35515
36525
  const { firstIndex, rowCount } = visibleListWindow({ sessionCount, selectedIndex, height });
35516
36526
  return Math.max(0, sessionCount - (firstIndex + rowCount));
35517
36527
  }
36528
+ function createFilterMode({ kind, filters }) {
36529
+ return {
36530
+ kind,
36531
+ selectedIndex: 0,
36532
+ statuses: [...filters.statuses],
36533
+ creators: [...filters.creators],
36534
+ query: "",
36535
+ searching: false
36536
+ };
36537
+ }
36538
+ function formatFilterMenu({ mode, authors, width }) {
36539
+ const options = visibleDashboardFilterOptions({ mode, authors });
36540
+ const selectedCount = mode.kind === "status" ? mode.statuses.length : mode.creators.length;
36541
+ const heading = `${mode.kind === "status" ? "Filter by status" : "Filter by author"} \xB7 ${selectedCount} selected \xB7 empty shows all`;
36542
+ const rows = options.map((option, index) => {
36543
+ const active = isDashboardFilterOptionSelected({ mode, option });
36544
+ const label = `${active ? "[x]" : "[ ]"} ${truncate(option.label, Math.max(1, width - 4))}`;
36545
+ const chunk = index === mode.selectedIndex ? bg(PALETTE.selectionBg)(fg3(PALETTE.bodyText)(label.padEnd(width))) : active ? bold2(fg3(PALETTE.humanAccent)(label)) : fg3(PALETTE.dimText)(label);
36546
+ return [chunk];
36547
+ });
36548
+ return joinStyled([
36549
+ [bold2(fg3(PALETTE.bodyText)(truncate(heading, width)))],
36550
+ ...rows.length > 0 ? rows : [[fg3(PALETTE.dimText)("No matching options.")]]
36551
+ ], `
36552
+ `);
36553
+ }
36554
+ function dashboardFilterOptions({ kind, authors }) {
36555
+ return kind === "status" ? lifecycleStatusOptions.map((option) => ({ kind: "status", id: option.value, label: option.label })) : authors.map((author) => ({ kind: "creator", id: author.id, label: author.name }));
36556
+ }
36557
+ function visibleDashboardFilterOptions({ mode, authors }) {
36558
+ return dashboardFilterOptions({ kind: mode.kind, authors }).filter((option) => fuzzyMatches(option.label, mode.query));
36559
+ }
36560
+ function isDashboardFilterOptionSelected({ mode, option }) {
36561
+ return option.kind === "status" ? mode.statuses.includes(option.id) : mode.creators.some((creator) => creator.id === option.id);
36562
+ }
36563
+ function updateDashboardFilterSelection({ mode, option, selected, authors }) {
36564
+ if (option.kind === "status") {
36565
+ const selectedStatuses = new Set(mode.statuses);
36566
+ if (selected)
36567
+ selectedStatuses.add(option.id);
36568
+ else
36569
+ selectedStatuses.delete(option.id);
36570
+ return {
36571
+ ...mode,
36572
+ statuses: lifecycleStatusOptions.map((entry) => entry.value).filter((status) => selectedStatuses.has(status))
36573
+ };
36574
+ }
36575
+ const selectedCreatorIds = new Set(mode.creators.map((creator) => creator.id));
36576
+ if (selected)
36577
+ selectedCreatorIds.add(option.id);
36578
+ else
36579
+ selectedCreatorIds.delete(option.id);
36580
+ return {
36581
+ ...mode,
36582
+ creators: authors.filter((author) => selectedCreatorIds.has(author.id))
36583
+ };
36584
+ }
35518
36585
  function formatSessionList({ sessions, selectedIndex, width, height }) {
35519
36586
  const { firstIndex, rowCount } = visibleListWindow({ sessionCount: sessions.length, selectedIndex, height });
35520
36587
  const visibleSessions = sessions.slice(firstIndex, firstIndex + rowCount);
@@ -35601,18 +36668,34 @@ function formatUpdatedAt(value) {
35601
36668
  return `${Math.floor(seconds / (60 * 60))}h ago`;
35602
36669
  return new Date(milliseconds).toLocaleDateString(undefined, { month: "short", day: "numeric" });
35603
36670
  }
35604
- function dashboardStatusBand({ page, pageIndex }) {
35605
- const openCount = page.sessions.filter((session) => session.status === "open").length;
36671
+ function matchesSessionSearch({ session, query }) {
36672
+ return fuzzyMatches(session.id, query) || fuzzyMatches(session.title ?? "", query) || fuzzyMatches(String(session.sessionNumber), query);
36673
+ }
36674
+ function visibleDashboardSessions({ sessions, query }) {
36675
+ return sessions.filter((session) => matchesSessionSearch({ session, query }));
36676
+ }
36677
+ function dashboardStatusBand({ page, pageIndex, filters, sessionSearchQuery }) {
36678
+ const openCount = visibleDashboardSessions({ sessions: page.sessions, query: sessionSearchQuery }).filter((session) => session.status === "open").length;
35606
36679
  const paging = [`page ${pageIndex}`];
35607
36680
  if (page.canGoPrevious)
35608
36681
  paging.push("\u2190 prev");
35609
36682
  if (page.hasMore)
35610
36683
  paging.push("more \u2192");
35611
- return `remy \xB7 sessions \xB7 ${openCount} open \xB7 ${paging.join(" \xB7 ")}`;
36684
+ const activeFilters = [
36685
+ filters.statuses.length > 0 ? `status: ${filters.statuses.map(sessionStatusLabel).join(", ")}` : undefined,
36686
+ filters.creators.length > 0 ? `author: ${filters.creators.map((creator) => creator.name).join(", ")}` : undefined,
36687
+ sessionSearchQuery ? `search: ${sessionSearchQuery}` : undefined
36688
+ ].filter((value) => value !== undefined);
36689
+ return `remy \xB7 sessions \xB7 ${openCount} open${activeFilters.length > 0 ? ` \xB7 ${activeFilters.join(" \xB7 ")}` : ""} \xB7 ${paging.join(" \xB7 ")}`;
35612
36690
  }
35613
- function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen }) {
36691
+ function hasActiveFilters(filters) {
36692
+ return filters.statuses.length > 0 || filters.creators.length > 0;
36693
+ }
36694
+ function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmationOpen, sessionSearchOpen }) {
35614
36695
  if (logoutConfirmationOpen)
35615
36696
  return "Log out of Remy? y confirms \xB7 Esc cancels";
36697
+ if (sessionSearchOpen)
36698
+ return "type to search \xB7 \u23CE apply search \xB7 Esc clear search";
35616
36699
  const parts = [];
35617
36700
  if (loadingPage)
35618
36701
  parts.push("Loading sessions\u2026");
@@ -35620,9 +36703,22 @@ function dashboardFooter({ loadingPage, pageError, rowsBelow, logoutConfirmation
35620
36703
  parts.push(`Could not load sessions: ${pageError}`);
35621
36704
  if (rowsBelow > 0)
35622
36705
  parts.push(`\u2193 ${rowsBelow} more below`);
35623
- parts.push("\u2191\u2193 move \xB7 \u23CE open \xB7 n new session \xB7 \u21E7l log out \xB7 q quit");
36706
+ parts.push("\u2191\u2193 move \xB7 \u23CE open \xB7 s search \xB7 f status \xB7 a author \xB7 n new session \xB7 \u21E7l log out \xB7 q quit");
35624
36707
  return parts.join(" \xB7 ");
35625
36708
  }
36709
+ function dashboardFilterFooter({ mode, loadingPage, loadingAuthors, pageError, authorError }) {
36710
+ if (loadingAuthors)
36711
+ return "Loading authors\u2026 \xB7 Esc cancel";
36712
+ if (loadingPage)
36713
+ return "Applying filter\u2026";
36714
+ if (mode.searching)
36715
+ return "type to filter \xB7 \u23CE apply search \xB7 Esc cancel search";
36716
+ return [
36717
+ authorError ? `Could not load authors: ${authorError}` : undefined,
36718
+ pageError ? `Could not apply filter: ${pageError}` : undefined,
36719
+ "\u2191\u2193/jk move \xB7 space toggle \xB7 \u2190\u2192/hl mark \xB7 s search \xB7 \u23CE apply \xB7 Esc cancel"
36720
+ ].filter((value) => value !== undefined).join(" \xB7 ");
36721
+ }
35626
36722
  function truncate(value, width) {
35627
36723
  if (value.length <= width)
35628
36724
  return value;
@@ -35633,87 +36729,33 @@ async function createDefaultRenderer() {
35633
36729
  }
35634
36730
 
35635
36731
  // src/tui/new-session-wizard.ts
35636
- 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";
35637
-
35638
- // src/tui/composer.ts
35639
- import { BoxRenderable as BoxRenderable2, TextRenderable as TextRenderable2, TextareaRenderable } from "@opentui/core";
36732
+ import { bg as bg3, BoxRenderable as BoxRenderable3, bold as bold3, CliRenderEvents as CliRenderEvents3, dim as dim4, fg as fg6, ScrollBoxRenderable as ScrollBoxRenderable2, StyledText as StyledText5, stringToStyledText as stringToStyledText4, TextRenderable as TextRenderable3 } from "@opentui/core";
35640
36733
 
35641
- // src/tui/composer-height.ts
35642
- var composerMaxRows = 6;
35643
- function resizeComposer(composer) {
35644
- composer.height = Math.min(Math.max(composer.editorView.getTotalVirtualLineCount(), 1), composerMaxRows);
36734
+ // src/tui/attachment-presentation.ts
36735
+ import { dim as dim3, fg as fg4, StyledText as StyledText3 } from "@opentui/core";
36736
+ function attachmentUploadMessage(filenames) {
36737
+ return filenames.length === 1 ? `Attaching ${filenames[0]}\u2026` : `Attaching ${filenames.length} files\u2026`;
36738
+ }
36739
+ function renderAttachmentFeedback(feedback) {
36740
+ const presentation = feedback.kind === "progress" ? { glyph: "\u28FF", color: PALETTE.progress } : feedback.kind === "success" ? { glyph: "\u2713", color: PALETTE.statusCompleted } : { glyph: "\u2717", color: PALETTE.failure };
36741
+ return new StyledText3([
36742
+ fg4(presentation.color)(`${presentation.glyph} `),
36743
+ fg4(feedback.kind === "failure" ? PALETTE.failure : PALETTE.bodyText)(feedback.message)
36744
+ ]);
35645
36745
  }
35646
-
35647
- // src/tui/composer.ts
35648
- var defaultPlaceholder = "Message Remy\u2026 Tab paths attach \xB7 Enter send \xB7 Shift+Enter/Ctrl+J newline";
35649
- function createComposer({
35650
- renderer,
35651
- parent,
35652
- tag,
35653
- topDividerLabel,
35654
- placeholder = defaultPlaceholder,
35655
- onSubmit,
35656
- onContentChange
35657
- }) {
35658
- const root = new BoxRenderable2(renderer, { width: "100%", flexDirection: "column", flexShrink: 0 });
35659
- const dividerTop = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
35660
- const textarea = new TextareaRenderable(renderer, {
35661
- width: "100%",
35662
- height: 1,
35663
- flexShrink: 0,
35664
- wrapMode: "word",
35665
- placeholder,
35666
- keyBindings: [
35667
- { name: "return", action: "submit" },
35668
- { name: "return", shift: true, action: "newline" },
35669
- { name: "j", ctrl: true, action: "newline" }
35670
- ],
35671
- onSubmit
35672
- });
35673
- const dividerBottom = new TextRenderable2(renderer, { content: "", flexShrink: 0 });
35674
- let mounted = false;
35675
- root.add(dividerTop);
35676
- root.add(textarea);
35677
- root.add(dividerBottom);
35678
- textarea.onContentChange = () => {
35679
- resizeComposer(textarea);
35680
- onContentChange?.();
35681
- };
35682
- function render() {
35683
- dividerTop.content = renderComposerDivider({ width: renderer.width - 2, tag, leadLabel: topDividerLabel?.() });
35684
- dividerBottom.content = renderComposerDivider({ width: renderer.width - 2 });
35685
- }
35686
- return {
35687
- textarea,
35688
- mount() {
35689
- if (mounted)
35690
- return;
35691
- mounted = true;
35692
- parent.add(root);
35693
- resizeComposer(textarea);
35694
- render();
35695
- },
35696
- unmount() {
35697
- if (!mounted)
35698
- return;
35699
- mounted = false;
35700
- parent.remove(root);
35701
- },
35702
- render,
35703
- focus() {
35704
- textarea.focus();
35705
- },
35706
- destroy() {
35707
- if (mounted)
35708
- parent.remove(root);
35709
- mounted = false;
35710
- root.destroyRecursively();
35711
- }
35712
- };
36746
+ function renderAttachmentSummary(attachments) {
36747
+ if (attachments.length === 0)
36748
+ return new StyledText3([]);
36749
+ const label = attachments.length === 1 ? "Attachment" : `${attachments.length} attachments`;
36750
+ return new StyledText3([
36751
+ dim3(fg4(PALETTE.dimText)("\u21B3 ")),
36752
+ fg4(PALETTE.tool)(label),
36753
+ dim3(fg4(PALETTE.dimText)(` \xB7 ${attachments.map((attachment) => attachment.filename).join(" \xB7 ")}`))
36754
+ ]);
35713
36755
  }
35714
36756
 
35715
36757
  // src/tui/path-completion-menu.ts
35716
- import { bg as bg2, fg as fg4, stringToStyledText as stringToStyledText3, StyledText as StyledText3 } from "@opentui/core";
36758
+ import { bg as bg2, fg as fg5, stringToStyledText as stringToStyledText3, StyledText as StyledText4 } from "@opentui/core";
35717
36759
  var maximumVisibleCompletions = 8;
35718
36760
  function renderPathCompletionMenu({ completions, selectedIndex }) {
35719
36761
  if (completions.length === 0)
@@ -35724,7 +36766,7 @@ function renderPathCompletionMenu({ completions, selectedIndex }) {
35724
36766
  const completionIndex = firstIndex + index;
35725
36767
  const selected = completionIndex === selectedIndex;
35726
36768
  const rowStyle = selected ? bg2(PALETTE.selectionBg) : (chunk) => chunk;
35727
- return new StyledText3([rowStyle(fg4(PALETTE.bodyText)(`${selected ? "\u203A " : " "}${completion.display}`))]);
36769
+ return new StyledText4([rowStyle(fg5(PALETTE.bodyText)(`${selected ? "\u203A " : " "}${completion.display}`))]);
35728
36770
  });
35729
36771
  const overflow = completions.length > visibleCompletions.length ? stringToStyledText3(`${firstIndex + 1}-${firstIndex + visibleCompletions.length} of ${completions.length}`) : undefined;
35730
36772
  return joinStyled([
@@ -35764,13 +36806,62 @@ async function completePathToken({ text, cwd, homeDirectory = homedir() }) {
35764
36806
  const normalized = replacementPath.split(sep).join("/");
35765
36807
  const suffix = entry.isDirectory() ? "/" : "";
35766
36808
  const completed = `${normalized}${suffix}`;
35767
- return { display: completed, replacement: `${token.startsWith("@") ? "@" : ""}${completed}` };
36809
+ return {
36810
+ display: completed,
36811
+ replacement: `${token.startsWith("@") ? "@" : ""}${completed}`,
36812
+ ...entry.isFile() ? { attachmentPath: completedPath } : {}
36813
+ };
35768
36814
  });
35769
36815
  }
35770
36816
  function replaceActivePathToken({ text, replacement }) {
35771
36817
  const token = activePathToken(text);
35772
36818
  return token === undefined ? text : `${text.slice(0, text.length - token.length)}${replacement}`;
35773
36819
  }
36820
+ function selectedAttachmentPaths({ text, selections }) {
36821
+ return selections.filter((selection) => text.slice(selection.start, selection.end) === selection.token && hasPathTokenBoundaries({ text, selection })).map((selection) => selection.attachmentPath);
36822
+ }
36823
+ function createSelectedPathCompletion({ attachmentPath, token, end }) {
36824
+ return { attachmentPath, token, start: end - token.length, end };
36825
+ }
36826
+ function reconcileSelectedPathCompletions({
36827
+ previousText,
36828
+ text,
36829
+ selections
36830
+ }) {
36831
+ if (text === previousText)
36832
+ return selections;
36833
+ let changeStart = 0;
36834
+ while (changeStart < previousText.length && changeStart < text.length && previousText[changeStart] === text[changeStart])
36835
+ changeStart += 1;
36836
+ let previousChangeEnd = previousText.length;
36837
+ let nextChangeEnd = text.length;
36838
+ while (previousChangeEnd > changeStart && nextChangeEnd > changeStart && previousText[previousChangeEnd - 1] === text[nextChangeEnd - 1]) {
36839
+ previousChangeEnd -= 1;
36840
+ nextChangeEnd -= 1;
36841
+ }
36842
+ const offsetChange = nextChangeEnd - previousChangeEnd;
36843
+ return selections.flatMap((selection) => {
36844
+ if (selection.end <= changeStart)
36845
+ return [selection];
36846
+ if (selection.start >= previousChangeEnd) {
36847
+ return [{
36848
+ ...selection,
36849
+ start: selection.start + offsetChange,
36850
+ end: selection.end + offsetChange
36851
+ }];
36852
+ }
36853
+ return [];
36854
+ }).filter((selection) => hasPathTokenBoundaries({ text, selection }));
36855
+ }
36856
+ function hasPathTokenBoundaries({ text, selection }) {
36857
+ let before = selection.start;
36858
+ while (before > 0 && "([{'\"`".includes(text[before - 1]))
36859
+ before -= 1;
36860
+ let after = selection.end;
36861
+ while (after < text.length && '),.;:!?]}"`'.includes(text[after]))
36862
+ after += 1;
36863
+ return (before === 0 || /\s/.test(text[before - 1])) && (after === text.length || /\s/.test(text[after]));
36864
+ }
35774
36865
  function activePathToken(text) {
35775
36866
  const match = text.match(/(?:^|\s)(\S*)$/);
35776
36867
  return match?.[1];
@@ -35783,6 +36874,7 @@ async function createNewSessionWizard({
35783
36874
  repositories,
35784
36875
  reloadRepositories,
35785
36876
  suggestRepositories,
36877
+ suggestBranches,
35786
36878
  readClipboardImage,
35787
36879
  savePastedImage,
35788
36880
  attachPromptPaths,
@@ -35819,15 +36911,19 @@ async function createNewSessionWizard({
35819
36911
  else
35820
36912
  overrides.set(repositoryId, isSelected);
35821
36913
  }, renderRepositoryLoadingSkeleton = function() {
35822
- return new StyledText4([
35823
- dim3(fg5(PALETTE.dimText)(`\u283F \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
36914
+ return new StyledText5([
36915
+ dim4(fg6(PALETTE.dimText)(`\u283F \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
35824
36916
  \u283F \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
35825
36917
  \u283F \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588`))
35826
36918
  ]);
35827
36919
  }, render = function() {
35828
36920
  const repositoryListVisible = step === "repositories" || step === "repositorySearch";
35829
36921
  const composerVisible = step === "prompt" || step === "repositorySearch" || step === "recompute";
35830
- const orient = (body) => joinStyled([renderStepIndicator({ step, hasInstallationChoice: installationIds.length > 1 }), body], `
36922
+ const orient = (body) => joinStyled([renderStepIndicator({
36923
+ step,
36924
+ hasInstallationChoice: installationIds.length > 1,
36925
+ hasBranchChoice: step === "loadingBranches" || branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch")
36926
+ }), body], `
35831
36927
  `);
35832
36928
  if (composerVisible && !composerMounted) {
35833
36929
  composerMounted = true;
@@ -35845,19 +36941,22 @@ async function createNewSessionWizard({
35845
36941
  repositoryRows.content = "";
35846
36942
  repositoryDivider.content = "";
35847
36943
  repositoryFooter.content = "";
35848
- const attachmentText = attachments.length > 0 ? stringToStyledText4(`Attachments: ${attachments.map((attachment) => attachment.filename).join(", ")}`) : undefined;
36944
+ const attachmentText = renderAttachmentSummary(attachments);
36945
+ const attachmentStatus = attachmentFeedback ? renderAttachmentFeedback(attachmentFeedback) : undefined;
35849
36946
  if (step === "prompt") {
35850
36947
  const parts = [
35851
- new StyledText4([stepHeader("New remote Codex session")]),
36948
+ new StyledText5([stepHeader("New remote Codex session")]),
35852
36949
  stringToStyledText4("Describe the work you want Remy to do. Press Enter to continue; Esc back.")
35853
36950
  ];
35854
- if (attachmentText)
36951
+ if (attachmentText.chunks.length > 0)
35855
36952
  parts.push(attachmentText);
35856
36953
  const completionMenu = pathCompletionMenuOpen ? renderPathCompletionMenu({ completions: pathCompletions, selectedIndex: completionIndex }) : undefined;
35857
36954
  if (completionMenu)
35858
36955
  parts.push(completionMenu);
35859
36956
  if (status)
35860
36957
  parts.push(stringToStyledText4(status));
36958
+ if (attachmentStatus)
36959
+ parts.push(attachmentStatus);
35861
36960
  content.content = orient(joinStyled(parts, `
35862
36961
 
35863
36962
  `));
@@ -35865,7 +36964,7 @@ async function createNewSessionWizard({
35865
36964
  const rows = joinStyled(installationIds.map((id, index) => renderSelectableRow({ label: id, isCursor: index === installationIndex })), `
35866
36965
  `);
35867
36966
  content.content = orient(joinStyled([
35868
- new StyledText4([stepHeader("Select GitHub installation")]),
36967
+ new StyledText5([stepHeader("Select GitHub installation")]),
35869
36968
  rows,
35870
36969
  stringToStyledText4("\u2191\u2193 move \xB7 \u23CE continue \xB7 esc back")
35871
36970
  ], `
@@ -35874,11 +36973,11 @@ async function createNewSessionWizard({
35874
36973
  } else if (step === "loadingRepositories") {
35875
36974
  const failed = repositoryLoadState === "failed";
35876
36975
  const parts = [
35877
- new StyledText4([stepHeader("Select repositories")]),
36976
+ new StyledText5([stepHeader("Select repositories")]),
35878
36977
  stringToStyledText4(failed ? "Could not load your repositories." : "Loading your repos\u2026")
35879
36978
  ];
35880
36979
  if (failed) {
35881
- parts.push(new StyledText4([dim3(fg5(PALETTE.dimText)(`(${status})`))]));
36980
+ parts.push(new StyledText5([dim4(fg6(PALETTE.dimText)(`(${status})`))]));
35882
36981
  parts.push(stringToStyledText4("r retry \xB7 esc back"));
35883
36982
  } else {
35884
36983
  parts.push(renderRepositoryLoadingSkeleton());
@@ -35889,12 +36988,41 @@ async function createNewSessionWizard({
35889
36988
  `));
35890
36989
  } else if (step === "loadingSuggestions") {
35891
36990
  content.content = orient(joinStyled([
35892
- new StyledText4([stepHeader("Select repositories")]),
35893
- new StyledText4([fg5(PALETTE.progress)(`${suggestionSpinnerFrames[suggestionSpinnerFrame]} Remy is matching your prompt to repositories\u2026`)]),
35894
- new StyledText4([dim3(fg5(PALETTE.dimText)("This can take a few seconds."))]),
36991
+ new StyledText5([stepHeader("Select repositories")]),
36992
+ new StyledText5([fg6(PALETTE.progress)(`${suggestionSpinnerFrames[suggestionSpinnerFrame]} Remy is matching your prompt to repositories\u2026`)]),
36993
+ new StyledText5([dim4(fg6(PALETTE.dimText)("This can take a few seconds."))]),
35895
36994
  stringToStyledText4("s select repositories yourself \xB7 esc back")
35896
36995
  ], `
35897
36996
 
36997
+ `));
36998
+ } else if (step === "loadingBranches") {
36999
+ content.content = orient(joinStyled([
37000
+ new StyledText5([stepHeader("Choose branches")]),
37001
+ new StyledText5([fg6(PALETTE.progress)(`${suggestionSpinnerFrames[suggestionSpinnerFrame]} Remy is checking for related open pull requests\u2026`)]),
37002
+ new StyledText5([dim4(fg6(PALETTE.dimText)("This can take a few seconds."))]),
37003
+ stringToStyledText4("esc back")
37004
+ ], `
37005
+
37006
+ `));
37007
+ } else if (step === "branches") {
37008
+ const existing = branchSuggestions.filter((suggestion) => suggestion.suggestionKind !== "createSessionBranch");
37009
+ const parts = [new StyledText5([stepHeader("Choose branches")])];
37010
+ if (existing.length > 0) {
37011
+ parts.push(stringToStyledText4("Remy found related open pull-request work:"));
37012
+ for (const suggestion of existing) {
37013
+ const repository = installationRepositories().find((candidate) => candidate.id === suggestion.repositoryId);
37014
+ parts.push(stringToStyledText4(`${repository?.fullName ?? suggestion.repositoryId} \u2014 ${suggestion.branchName}${suggestion.pullRequest ? ` (PR #${suggestion.pullRequest.number}: ${suggestion.pullRequest.title})` : ""}`));
37015
+ }
37016
+ parts.push(new StyledText5(renderSelectableRow({ label: "Continue from the suggested branch", isCursor: branchChoice === "existing" })));
37017
+ parts.push(new StyledText5(renderSelectableRow({ label: "Create a new session branch", isCursor: branchChoice === "new" })));
37018
+ parts.push(stringToStyledText4("\u2191\u2193 choose \xB7 \u23CE continue \xB7 esc back"));
37019
+ } else {
37020
+ parts.push(stringToStyledText4("Remy recommends creating a new session branch."));
37021
+ parts.push(new StyledText5([dim4(fg6(PALETTE.dimText)(status || "No related open pull request was identified from your request."))]));
37022
+ parts.push(stringToStyledText4("\u23CE continue \xB7 esc back"));
37023
+ }
37024
+ content.content = orient(joinStyled(parts, `
37025
+
35898
37026
  `));
35899
37027
  } else if (repositoryListVisible) {
35900
37028
  const selectionOverrides = searchSelections ?? manualSelections;
@@ -35909,15 +37037,17 @@ async function createNewSessionWizard({
35909
37037
  `) : step === "repositorySearch" ? "No matching repositories." : "No repositories available for this installation.";
35910
37038
  repositoryDivider.content = renderComposerDivider({ width: renderer.width - 2 });
35911
37039
  repositoryFooter.content = step === "repositorySearch" ? "type to filter \xB7 \u23CE apply \xB7 esc cancel search" : "\u2191\u2193 move \xB7 space toggle \xB7 \u2190\u2192 mark \xB7 s search \xB7 r recompute \xB7 \u23CE continue \xB7 esc back";
35912
- const parts = step === "repositorySearch" ? [new StyledText4([stepHeader("Search repositories")]), stringToStyledText4("Fuzzy matching repository names.")] : [
35913
- new StyledText4([stepHeader("Select repositories")]),
37040
+ const parts = step === "repositorySearch" ? [new StyledText5([stepHeader("Search repositories")]), stringToStyledText4("Fuzzy matching repository names.")] : [
37041
+ new StyledText5([stepHeader("Select repositories")]),
35914
37042
  stringToStyledText4(`${selected.size} selected \xB7 ${suggestedIds.size} suggested by Remy${suggestionConfidence ? ` (${suggestionConfidence} confidence)` : ""}`),
35915
37043
  stringToStyledText4("Suggestions are based on your prompt and recompute instruction.")
35916
37044
  ];
35917
- if (attachmentText)
37045
+ if (attachmentText.chunks.length > 0)
35918
37046
  parts.push(attachmentText);
35919
37047
  if (status && step === "repositories")
35920
37048
  parts.push(stringToStyledText4(status));
37049
+ if (attachmentStatus)
37050
+ parts.push(attachmentStatus);
35921
37051
  content.content = orient(joinStyled(parts, `
35922
37052
 
35923
37053
  `));
@@ -35925,11 +37055,11 @@ async function createNewSessionWizard({
35925
37055
  editor.placeholder = "Filter repositories\u2026";
35926
37056
  } else if (step === "recompute") {
35927
37057
  const parts = [
35928
- new StyledText4([stepHeader("Recompute suggestions")]),
37058
+ new StyledText5([stepHeader("Recompute suggestions")]),
35929
37059
  stringToStyledText4("Suggestions use your prompt and instruction. Existing manual toggles stay selected or unselected."),
35930
37060
  stringToStyledText4("Enter instruction, then press Enter to continue. Esc back.")
35931
37061
  ];
35932
- if (attachmentText)
37062
+ if (attachmentText.chunks.length > 0)
35933
37063
  parts.push(attachmentText);
35934
37064
  content.content = orient(joinStyled(parts, `
35935
37065
 
@@ -35937,26 +37067,31 @@ async function createNewSessionWizard({
35937
37067
  editor.placeholder = "e.g. worker, migration, dashboard";
35938
37068
  } else {
35939
37069
  const selected = installationRepositories().filter((repository) => selectedRepositoryIds().has(repository.id));
35940
- const meta5 = new StyledText4([
35941
- fg5(PALETTE.bodyText)(`Installation: ${selectedInstallationId() ?? "(missing)"}
37070
+ const branchOverrides = branchChoice === "existing" ? branchSuggestions.filter((suggestion) => suggestion.suggestionKind !== "createSessionBranch") : [];
37071
+ const meta5 = new StyledText5([
37072
+ fg6(PALETTE.bodyText)(`Installation: ${selectedInstallationId() ?? "(missing)"}
35942
37073
  `),
35943
- fg5(PALETTE.bodyText)(`Repositories: ${selected.map((repository) => repository.fullName).join(", ") || "(none)"}
37074
+ fg6(PALETTE.bodyText)(`Repositories: ${selected.map((repository) => repository.fullName).join(", ") || "(none)"}
35944
37075
  `),
35945
- fg5(PALETTE.bodyText)(`Model: ${newSessionModelLabel(model)} \xB7 ${newSessionReasoningEffortLabel(reasoningEffort)} reasoning`),
35946
- dim3(fg5(PALETTE.dimText)(" \u25B8 m model \xB7 \u2190\u2192 reasoning"))
37076
+ fg6(PALETTE.bodyText)(`Branches: ${branchOverrides.length > 0 ? branchOverrides.map((override) => override.branchName).join(", ") : "new session branches"}
37077
+ `),
37078
+ fg6(PALETTE.bodyText)(`Model: ${newSessionModelLabel(model)} \xB7 ${newSessionReasoningEffortLabel(reasoningEffort)} reasoning`),
37079
+ dim4(fg6(PALETTE.dimText)(" \u25B8 m model \xB7 \u2190\u2192 reasoning"))
35947
37080
  ]);
35948
37081
  const parts = [
35949
- new StyledText4([stepHeader("Confirm new session")]),
35950
- new StyledText4([renderRoleLabel({ label: "You", role: "human" })]),
37082
+ new StyledText5([stepHeader("Confirm new session")]),
37083
+ new StyledText5([renderRoleLabel({ label: "You", role: "human" })]),
35951
37084
  renderMessageBody({ text: prompt, role: "human" }),
35952
37085
  meta5
35953
37086
  ];
35954
- if (attachmentText)
37087
+ if (attachmentText.chunks.length > 0)
35955
37088
  parts.push(attachmentText);
35956
- 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."))]));
35957
- parts.push(stringToStyledText4("\u23CE create \xB7 e edit prompt \xB7 r repositories \xB7 esc back"));
37089
+ parts.push(new StyledText5([dim4(fg6(PALETTE.dimText)(branchOverrides.length > 0 ? "Remy will continue on the selected existing branch and update its pull request; you land in the session view." : "Remy will clone the selected repositories, work on new session branches, and open a pull request for each repository; you land in the session view."))]));
37090
+ parts.push(stringToStyledText4(branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch") ? "\u23CE create \xB7 e edit prompt \xB7 r repositories \xB7 b branches \xB7 esc back" : "\u23CE create \xB7 e edit prompt \xB7 r repositories \xB7 esc back"));
35958
37091
  if (status)
35959
37092
  parts.push(stringToStyledText4(status));
37093
+ if (attachmentStatus)
37094
+ parts.push(attachmentStatus);
35960
37095
  content.content = orient(joinStyled(parts, `
35961
37096
 
35962
37097
  `));
@@ -35981,7 +37116,7 @@ async function createNewSessionWizard({
35981
37116
  syncSuggestionLoadingIndicator();
35982
37117
  renderer.requestRender();
35983
37118
  }, syncSuggestionLoadingIndicator = function() {
35984
- if (destroyed || settled || step !== "loadingSuggestions") {
37119
+ if (destroyed || settled || step !== "loadingSuggestions" && step !== "loadingBranches") {
35985
37120
  if (suggestionSpinnerTimer) {
35986
37121
  clearInterval(suggestionSpinnerTimer);
35987
37122
  suggestionSpinnerTimer = undefined;
@@ -35992,22 +37127,28 @@ async function createNewSessionWizard({
35992
37127
  if (suggestionSpinnerTimer)
35993
37128
  return;
35994
37129
  suggestionSpinnerTimer = setInterval(() => {
35995
- if (destroyed || settled || step !== "loadingSuggestions") {
37130
+ if (destroyed || settled || step !== "loadingSuggestions" && step !== "loadingBranches") {
35996
37131
  syncSuggestionLoadingIndicator();
35997
37132
  return;
35998
37133
  }
35999
37134
  suggestionSpinnerFrame = (suggestionSpinnerFrame + 1) % suggestionSpinnerFrames.length;
36000
37135
  render();
36001
37136
  }, suggestionSpinnerIntervalMs);
37137
+ }, invalidatePendingConfirmation = function() {
37138
+ pendingConfirmation = undefined;
37139
+ if (attachmentFeedback?.kind === "progress")
37140
+ attachmentFeedback = undefined;
36002
37141
  }, finish = function(value) {
36003
37142
  if (settled)
36004
37143
  return;
37144
+ invalidatePendingConfirmation();
36005
37145
  settled = true;
36006
37146
  syncSuggestionLoadingIndicator();
36007
37147
  resolveDraft(value);
36008
37148
  }, fail = function(error93) {
36009
37149
  if (settled)
36010
37150
  return;
37151
+ invalidatePendingConfirmation();
36011
37152
  settled = true;
36012
37153
  syncSuggestionLoadingIndicator();
36013
37154
  rejectDraft(error93);
@@ -36078,7 +37219,15 @@ async function createNewSessionWizard({
36078
37219
  returnToPromptFromRepositoryLoading();
36079
37220
  return;
36080
37221
  }
37222
+ if (step === "loadingBranches" || step === "branches") {
37223
+ branchRequestGeneration += 1;
37224
+ step = "repositories";
37225
+ render();
37226
+ return;
37227
+ }
36081
37228
  if (step === "recompute" || step === "confirmation") {
37229
+ if (step === "confirmation")
37230
+ invalidatePendingConfirmation();
36082
37231
  step = "repositories";
36083
37232
  editor.setText("");
36084
37233
  render();
@@ -36106,6 +37255,15 @@ async function createNewSessionWizard({
36106
37255
  const completedText = replaceActivePathToken({ text: editor.plainText, replacement: completion.replacement });
36107
37256
  editor.setText(completedText);
36108
37257
  editor.cursorOffset = completedText.length;
37258
+ if (completion.attachmentPath) {
37259
+ const selectedCompletion = createSelectedPathCompletion({
37260
+ attachmentPath: completion.attachmentPath,
37261
+ token: completion.replacement,
37262
+ end: completedText.length
37263
+ });
37264
+ selectedPathCompletions = [...selectedPathCompletions, selectedCompletion];
37265
+ }
37266
+ previousPromptEditorText = completedText;
36109
37267
  }
36110
37268
  pathCompletions = [];
36111
37269
  pathCompletionMenuOpen = false;
@@ -36131,12 +37289,14 @@ async function createNewSessionWizard({
36131
37289
  if (step === "confirmation") {
36132
37290
  if (key.name === "m") {
36133
37291
  key.preventDefault();
37292
+ invalidatePendingConfirmation();
36134
37293
  model = cycle({ values: newSessionModels, value: model, direction: 1 });
36135
37294
  render();
36136
37295
  return;
36137
37296
  }
36138
37297
  if (key.name === "left" || key.name === "right") {
36139
37298
  key.preventDefault();
37299
+ invalidatePendingConfirmation();
36140
37300
  reasoningEffort = clampedStep({
36141
37301
  values: newSessionReasoningEfforts,
36142
37302
  value: reasoningEffort,
@@ -36147,6 +37307,7 @@ async function createNewSessionWizard({
36147
37307
  }
36148
37308
  if (key.name === "e") {
36149
37309
  key.preventDefault();
37310
+ invalidatePendingConfirmation();
36150
37311
  step = "prompt";
36151
37312
  editor.setText(prompt);
36152
37313
  render();
@@ -36154,11 +37315,19 @@ async function createNewSessionWizard({
36154
37315
  }
36155
37316
  if (key.name === "r") {
36156
37317
  key.preventDefault();
37318
+ invalidatePendingConfirmation();
36157
37319
  step = "repositories";
36158
37320
  editor.setText("");
36159
37321
  render();
36160
37322
  return;
36161
37323
  }
37324
+ if (key.name === "b" && branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch")) {
37325
+ key.preventDefault();
37326
+ invalidatePendingConfirmation();
37327
+ step = "branches";
37328
+ render();
37329
+ return;
37330
+ }
36162
37331
  }
36163
37332
  if (step === "repositorySearch")
36164
37333
  return;
@@ -36179,8 +37348,16 @@ async function createNewSessionWizard({
36179
37348
  return;
36180
37349
  }
36181
37350
  }
37351
+ if (step === "branches" && branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch") && (key.name === "up" || key.name === "down" || key.name === "left" || key.name === "right")) {
37352
+ key.preventDefault();
37353
+ branchChoice = branchChoice === "existing" ? "new" : "existing";
37354
+ render();
37355
+ return;
37356
+ }
36182
37357
  if (!composerMounted && (key.name === "return" || key.name === "enter")) {
36183
37358
  key.preventDefault();
37359
+ if (step === "loadingBranches")
37360
+ return;
36184
37361
  submitEditor();
36185
37362
  return;
36186
37363
  }
@@ -36269,6 +37446,7 @@ async function createNewSessionWizard({
36269
37446
  let repositoryLoadState = Array.isArray(repositories) ? "ready" : "loading";
36270
37447
  let repositoryRequestGeneration = 0;
36271
37448
  let suggestionRequestGeneration = 0;
37449
+ let branchRequestGeneration = 0;
36272
37450
  let installationIds = [...new Set(loadedRepositories.map((repository) => repository.githubInstallationId))].sort((left, right) => left.localeCompare(right));
36273
37451
  let step = initialDraft ? "confirmation" : "prompt";
36274
37452
  let prompt = initialDraft?.prompt ?? "";
@@ -36280,7 +37458,9 @@ async function createNewSessionWizard({
36280
37458
  let recomputeInstruction = "";
36281
37459
  let suggestionSpinnerFrame = 0;
36282
37460
  let attachments = [...initialDraft?.attachments ?? []];
37461
+ let previousPromptEditorText = "";
36283
37462
  let pathCompletions = [];
37463
+ let selectedPathCompletions = [];
36284
37464
  let completionIndex = 0;
36285
37465
  let pathCompletionMenuOpen = false;
36286
37466
  let repositoryHeaderHeight;
@@ -36289,8 +37469,17 @@ async function createNewSessionWizard({
36289
37469
  let searchSelections;
36290
37470
  let suggestedIds = new Set;
36291
37471
  let suggestionConfidence;
37472
+ let branchSuggestions = initialDraft?.repositoryBranchOverrides?.map((override) => ({
37473
+ suggestionKind: "continueExistingBranch",
37474
+ repositoryId: override.repositoryId,
37475
+ branchName: override.branchName,
37476
+ pullRequest: null
37477
+ })) ?? [];
37478
+ let branchChoice = initialDraft?.repositoryBranchOverrides?.length ? "existing" : "new";
36292
37479
  let status = initialError ?? "";
37480
+ let attachmentFeedback;
36293
37481
  let pendingPastedImageWrites = 0;
37482
+ let pendingConfirmation;
36294
37483
  let destroyed = false;
36295
37484
  let rendererDestroyPromise;
36296
37485
  let settled = false;
@@ -36313,6 +37502,14 @@ async function createNewSessionWizard({
36313
37502
  submitEditor();
36314
37503
  },
36315
37504
  onContentChange: () => {
37505
+ if (step === "prompt") {
37506
+ selectedPathCompletions = reconcileSelectedPathCompletions({
37507
+ previousText: previousPromptEditorText,
37508
+ text: editor.plainText,
37509
+ selections: selectedPathCompletions
37510
+ });
37511
+ previousPromptEditorText = editor.plainText;
37512
+ }
36316
37513
  pathCompletionMenuOpen = false;
36317
37514
  if (step === "repositorySearch") {
36318
37515
  repositoryQuery = editor.plainText;
@@ -36348,6 +37545,35 @@ async function createNewSessionWizard({
36348
37545
  status = error93 instanceof Error ? error93.message : String(error93);
36349
37546
  }
36350
37547
  }
37548
+ async function startBranchReview() {
37549
+ const installationId = selectedInstallationId();
37550
+ if (!installationId)
37551
+ return;
37552
+ const repositoryIds = [...selectedRepositoryIds()];
37553
+ const requestGeneration = ++branchRequestGeneration;
37554
+ branchSuggestions = [];
37555
+ branchChoice = "new";
37556
+ step = "loadingBranches";
37557
+ status = "";
37558
+ render();
37559
+ try {
37560
+ const suggestions = await suggestBranches({ installationId, prompt, repositoryIds });
37561
+ if (destroyed || requestGeneration !== branchRequestGeneration || step !== "loadingBranches")
37562
+ return;
37563
+ branchSuggestions = suggestions;
37564
+ const hasExistingBranch = branchSuggestions.some((suggestion) => suggestion.suggestionKind !== "createSessionBranch");
37565
+ branchChoice = hasExistingBranch ? "existing" : "new";
37566
+ step = hasExistingBranch ? "branches" : "confirmation";
37567
+ } catch (error93) {
37568
+ if (destroyed || requestGeneration !== branchRequestGeneration || step !== "loadingBranches")
37569
+ return;
37570
+ branchSuggestions = [];
37571
+ branchChoice = "new";
37572
+ status = error93 instanceof Error ? error93.message : String(error93);
37573
+ step = "confirmation";
37574
+ }
37575
+ render();
37576
+ }
36351
37577
  async function openPathCompletionMenu() {
36352
37578
  if (destroyed)
36353
37579
  return;
@@ -36379,28 +37605,33 @@ async function createNewSessionWizard({
36379
37605
  }
36380
37606
  async function pasteClipboardImage() {
36381
37607
  if (!readClipboardImage || !savePastedImage) {
36382
- status = "Clipboard image paste is unavailable.";
37608
+ attachmentFeedback = { kind: "failure", message: "Clipboard image paste is unavailable." };
36383
37609
  render();
36384
37610
  return;
36385
37611
  }
36386
37612
  pendingPastedImageWrites += 1;
36387
- status = "Saving clipboard image\u2026";
37613
+ attachmentFeedback = { kind: "progress", message: "Saving clipboard image\u2026" };
36388
37614
  render();
36389
37615
  try {
36390
37616
  const image = await readClipboardImage();
36391
37617
  const path = await savePastedImage(image);
36392
37618
  if (!destroyed) {
36393
37619
  editor.insertText(path);
36394
- status = "Clipboard image added; continue to attach it.";
37620
+ const selectedCompletion = createSelectedPathCompletion({ attachmentPath: path, token: path, end: editor.cursorOffset });
37621
+ selectedPathCompletions = [...selectedPathCompletions, selectedCompletion];
37622
+ previousPromptEditorText = editor.plainText;
37623
+ attachmentFeedback = { kind: "success", message: "Clipboard image ready to attach." };
36395
37624
  }
36396
37625
  } catch (error93) {
36397
- status = error93 instanceof Error ? error93.message : String(error93);
37626
+ attachmentFeedback = { kind: "failure", message: error93 instanceof Error ? error93.message : String(error93) };
36398
37627
  } finally {
36399
37628
  pendingPastedImageWrites -= 1;
36400
37629
  }
36401
37630
  render();
36402
37631
  }
36403
37632
  async function confirm() {
37633
+ if (destroyed || settled || step !== "confirmation" || pendingConfirmation)
37634
+ return;
36404
37635
  const installationId = selectedInstallationId();
36405
37636
  if (!installationId) {
36406
37637
  status = "GitHub installation selection is required.";
@@ -36412,22 +37643,55 @@ async function createNewSessionWizard({
36412
37643
  fail(new Error("Selected repositories must belong to the chosen GitHub installation."));
36413
37644
  return;
36414
37645
  }
37646
+ const repositoryBranchOverrides = branchChoice === "existing" ? branchSuggestions.flatMap((suggestion) => suggestion.suggestionKind === "createSessionBranch" ? [] : [{ repositoryId: suggestion.repositoryId, branchName: suggestion.branchName }]) : [];
37647
+ const confirmation = Symbol("confirmation");
37648
+ const confirmedAttachments = [...attachments];
37649
+ const confirmedPrompt = prompt;
37650
+ const confirmedModel = model;
37651
+ const confirmedReasoningEffort = reasoningEffort;
37652
+ const confirmedSelectedPaths = selectedAttachmentPaths({ text: confirmedPrompt, selections: selectedPathCompletions });
37653
+ pendingConfirmation = confirmation;
37654
+ const ownsConfirmation = () => !destroyed && !settled && step === "confirmation" && pendingConfirmation === confirmation;
36415
37655
  if (attachPromptPaths) {
36416
- status = "Attaching referenced files\u2026";
36417
- render();
37656
+ attachmentFeedback = undefined;
36418
37657
  try {
36419
37658
  const promptAttachments = await attachPromptPaths({
36420
- text: prompt,
36421
- excludedPaths: attachments.flatMap((attachment) => attachment.sourcePath ? [attachment.sourcePath] : [])
37659
+ text: confirmedPrompt,
37660
+ excludedPaths: confirmedAttachments.flatMap((attachment) => attachment.sourcePath ? [attachment.sourcePath] : []),
37661
+ selectedPaths: confirmedSelectedPaths,
37662
+ onUploadStart: ({ filenames }) => {
37663
+ if (!ownsConfirmation())
37664
+ return;
37665
+ attachmentFeedback = { kind: "progress", message: attachmentUploadMessage(filenames) };
37666
+ render();
37667
+ }
36422
37668
  });
36423
- attachments = [...attachments, ...promptAttachments];
37669
+ if (!ownsConfirmation())
37670
+ return;
37671
+ confirmedAttachments.push(...promptAttachments);
37672
+ attachments = confirmedAttachments;
37673
+ selectedPathCompletions = [];
37674
+ attachmentFeedback = undefined;
36424
37675
  } catch (error93) {
36425
- status = error93 instanceof Error ? error93.message : String(error93);
37676
+ if (!ownsConfirmation())
37677
+ return;
37678
+ pendingConfirmation = undefined;
37679
+ attachmentFeedback = { kind: "failure", message: error93 instanceof Error ? error93.message : String(error93) };
36426
37680
  render();
36427
37681
  return;
36428
37682
  }
36429
37683
  }
36430
- finish({ prompt, githubInstallationId: installationId, repositories: selected, attachments, model, reasoningEffort });
37684
+ if (!ownsConfirmation())
37685
+ return;
37686
+ finish({
37687
+ prompt: confirmedPrompt,
37688
+ githubInstallationId: installationId,
37689
+ repositories: selected,
37690
+ ...repositoryBranchOverrides.length > 0 ? { repositoryBranchOverrides } : {},
37691
+ attachments: confirmedAttachments,
37692
+ model: confirmedModel,
37693
+ reasoningEffort: confirmedReasoningEffort
37694
+ });
36431
37695
  }
36432
37696
  async function submitEditor() {
36433
37697
  const value = editor.plainText.trim();
@@ -36461,8 +37725,12 @@ async function createNewSessionWizard({
36461
37725
  return;
36462
37726
  }
36463
37727
  if (step === "repositories") {
36464
- step = "confirmation";
36465
37728
  editor.setText("");
37729
+ startBranchReview();
37730
+ return;
37731
+ }
37732
+ if (step === "branches") {
37733
+ step = "confirmation";
36466
37734
  render();
36467
37735
  return;
36468
37736
  }
@@ -36526,6 +37794,7 @@ async function createNewSessionWizard({
36526
37794
  destroy() {
36527
37795
  if (rendererDestroyed2)
36528
37796
  return;
37797
+ invalidatePendingConfirmation();
36529
37798
  destroyed = true;
36530
37799
  if (inputHandler)
36531
37800
  renderer.removeInputHandler(inputHandler);
@@ -36562,12 +37831,13 @@ async function createNewSessionWizard({
36562
37831
  }
36563
37832
  }
36564
37833
  function stepHeader(title) {
36565
- return bold3(fg5(PALETTE.remyAccent)(title));
37834
+ return bold3(fg6(PALETTE.remyAccent)(title));
36566
37835
  }
36567
37836
  var WIZARD_STEPS = [
36568
37837
  { key: "describe", label: "Describe" },
36569
37838
  { key: "installation", label: "Installation" },
36570
37839
  { key: "repositories", label: "Repositories" },
37840
+ { key: "branches", label: "Branches" },
36571
37841
  { key: "confirm", label: "Confirm" }
36572
37842
  ];
36573
37843
  function wizardStepKey(step) {
@@ -36575,52 +37845,41 @@ function wizardStepKey(step) {
36575
37845
  return "describe";
36576
37846
  if (step === "installation")
36577
37847
  return "installation";
37848
+ if (step === "loadingBranches" || step === "branches")
37849
+ return "branches";
36578
37850
  if (step === "confirmation")
36579
37851
  return "confirm";
36580
37852
  return "repositories";
36581
37853
  }
36582
- function renderStepIndicator({ step, hasInstallationChoice }) {
36583
- const steps = WIZARD_STEPS.filter((entry) => entry.key !== "installation" || hasInstallationChoice);
37854
+ function renderStepIndicator({ step, hasInstallationChoice, hasBranchChoice = false }) {
37855
+ const steps = WIZARD_STEPS.filter((entry) => (entry.key !== "installation" || hasInstallationChoice) && (entry.key !== "branches" || hasBranchChoice));
36584
37856
  const currentKey = wizardStepKey(step);
36585
37857
  const currentIndex = steps.findIndex((entry) => entry.key === currentKey);
36586
37858
  const position = currentIndex >= 0 ? currentIndex + 1 : steps.length;
36587
37859
  const crumbs = [];
36588
37860
  steps.forEach((entry, index) => {
36589
37861
  if (index > 0)
36590
- crumbs.push(fg5(PALETTE.dimText)(" \u203A "));
36591
- crumbs.push(entry.key === currentKey ? bold3(fg5(PALETTE.bodyText)(entry.label)) : fg5(PALETTE.dimText)(entry.label));
37862
+ crumbs.push(fg6(PALETTE.dimText)(" \u203A "));
37863
+ crumbs.push(entry.key === currentKey ? bold3(fg6(PALETTE.bodyText)(entry.label)) : fg6(PALETTE.dimText)(entry.label));
36592
37864
  });
36593
- return new StyledText4([
36594
- bold3(fg5(PALETTE.remyAccent)(`Step ${position} of ${steps.length}`)),
36595
- fg5(PALETTE.dimText)(" "),
37865
+ return new StyledText5([
37866
+ bold3(fg6(PALETTE.remyAccent)(`Step ${position} of ${steps.length}`)),
37867
+ fg6(PALETTE.dimText)(" "),
36596
37868
  ...crumbs
36597
37869
  ]);
36598
37870
  }
36599
37871
  function renderSelectableRow({ label, isCursor }) {
36600
37872
  const line = `${isCursor ? "> " : " "}${label}`;
36601
- const styled = fg5(PALETTE.bodyText)(line);
37873
+ const styled = fg6(PALETTE.bodyText)(line);
36602
37874
  return [isCursor ? bg3(PALETTE.selectionBg)(styled) : styled];
36603
37875
  }
36604
37876
  function renderRepositoryRow({ repository, isCursor, isChecked, source }) {
36605
37877
  const marker = isChecked ? "[x]" : "[ ]";
36606
37878
  const line = `${isCursor ? "> " : " "}${marker} ${repository.fullName}`;
36607
- const styled = isChecked ? bold3(fg5(PALETTE.humanAccent)(line)) : fg5(PALETTE.dimText)(line);
36608
- const tag = source === "suggested" && isChecked ? dim3(fg5(PALETTE.tool)(" \u25C6 suggested by Remy")) : undefined;
37879
+ const styled = isChecked ? bold3(fg6(PALETTE.humanAccent)(line)) : fg6(PALETTE.dimText)(line);
37880
+ const tag = source === "suggested" && isChecked ? dim4(fg6(PALETTE.tool)(" \u25C6 suggested by Remy")) : undefined;
36609
37881
  return [isCursor ? bg3(PALETTE.selectionBg)(styled) : styled, ...tag ? [tag] : []];
36610
37882
  }
36611
- function fuzzyMatches(value, query) {
36612
- const normalizedQuery = query.replaceAll(/\s/g, "").toLowerCase();
36613
- if (!normalizedQuery)
36614
- return true;
36615
- let queryIndex = 0;
36616
- for (const character of value.toLowerCase()) {
36617
- if (character === normalizedQuery[queryIndex])
36618
- queryIndex += 1;
36619
- if (queryIndex === normalizedQuery.length)
36620
- return true;
36621
- }
36622
- return false;
36623
- }
36624
37883
  function cycle({ values, value, direction }) {
36625
37884
  const currentIndex = values.indexOf(value);
36626
37885
  const nextIndex = (currentIndex + direction + values.length) % values.length;
@@ -36636,7 +37895,7 @@ async function createDefaultRenderer2() {
36636
37895
  }
36637
37896
 
36638
37897
  // src/tui/session-view.ts
36639
- import { CliRenderEvents as CliRenderEvents4, BoxRenderable as BoxRenderable4, bg as bg4, bold as bold4, dim as dim4, fg as fg6, ScrollBoxRenderable as ScrollBoxRenderable3, StyledText as StyledText5, stringToStyledText as stringToStyledText5, TextRenderable as TextRenderable4 } from "@opentui/core";
37898
+ import { CliRenderEvents as CliRenderEvents4, BoxRenderable as BoxRenderable4, bg as bg4, bold as bold4, dim as dim5, fg as fg7, ScrollBoxRenderable as ScrollBoxRenderable3, StyledText as StyledText6, stringToStyledText as stringToStyledText5, TextRenderable as TextRenderable4 } from "@opentui/core";
36640
37899
  var composerSlashCommands = [
36641
37900
  { value: "/sessions", description: "Back to the session list" },
36642
37901
  { value: "/complete", description: "Finish this session" },
@@ -36717,6 +37976,7 @@ async function createSessionTui({
36717
37976
  }
36718
37977
  promptHistoryIndex = direction === "up" ? Math.max(0, promptHistoryIndex - 1) : Math.min(promptHistory.length, promptHistoryIndex + 1);
36719
37978
  const text = promptHistoryIndex === promptHistory.length ? promptHistoryDraft : promptHistory[promptHistoryIndex];
37979
+ selectedPathCompletions = [];
36720
37980
  historyReplacement = text;
36721
37981
  composer.setText(text);
36722
37982
  composer.cursorOffset = text.length;
@@ -36739,10 +37999,10 @@ async function createSessionTui({
36739
37999
  syncTerminalSessionControls();
36740
38000
  syncWorkingIndicator();
36741
38001
  renderRetainedTranscript();
36742
- const assistantPreview = latestState.previews.assistantText ? new StyledText5([dim4(fg6(PALETTE.dimText)(`Live assistant preview
36743
- `)), fg6(PALETTE.remyAccent)(latestState.previews.assistantText)]) : new StyledText5([]);
36744
- const reasoningPreview = latestState.previews.reasoningText ? new StyledText5([dim4(fg6(PALETTE.dimText)(`Live reasoning preview
36745
- `)), dim4(fg6(PALETTE.dimText)(latestState.previews.reasoningText))]) : new StyledText5([]);
38002
+ const assistantPreview = latestState.previews.assistantText ? new StyledText6([dim5(fg7(PALETTE.dimText)(`Live assistant preview
38003
+ `)), fg7(PALETTE.remyAccent)(latestState.previews.assistantText)]) : new StyledText6([]);
38004
+ const reasoningPreview = latestState.previews.reasoningText ? new StyledText6([dim5(fg7(PALETTE.dimText)(`Live reasoning preview
38005
+ `)), dim5(fg7(PALETTE.dimText)(latestState.previews.reasoningText))]) : new StyledText6([]);
36746
38006
  const working = isRemyWorking(latestState);
36747
38007
  const startedAt = workingTurnStartedAt(latestState);
36748
38008
  const elapsedMs = startedAt === undefined ? 0 : Math.max(0, now3() - Date.parse(startedAt));
@@ -36751,7 +38011,7 @@ async function createSessionTui({
36751
38011
  frame: workingSpinnerFrames[spinnerFrameIndex],
36752
38012
  elapsedMs,
36753
38013
  mode: stopState.kind === "stopping" ? "stopping" : "working"
36754
- }) : new StyledText5([]);
38014
+ }) : new StyledText6([]);
36755
38015
  const liveContent = [assistantPreview, reasoningPreview, liveIndicator].filter((part) => part.chunks.length > 0);
36756
38016
  liveTranscript.content = liveContent.length === 0 ? "" : joinStyled([stringToStyledText5(`
36757
38017
 
@@ -36767,6 +38027,7 @@ async function createSessionTui({
36767
38027
  const commandMenu = renderSlashCommandMenu({ commands: slashCompletions, selectedIndex: slashCompletionIndex });
36768
38028
  const composerStatus = [
36769
38029
  helpVisible ? renderHelpPanel() : undefined,
38030
+ attachmentFeedback ? renderAttachmentFeedback(attachmentFeedback) : undefined,
36770
38031
  composerFeedback ? stringToStyledText5(composerFeedback) : undefined,
36771
38032
  submissionStatus,
36772
38033
  commandMenu,
@@ -36839,10 +38100,17 @@ async function createSessionTui({
36839
38100
  }
36840
38101
  slashCompletions = composerSlashCommands.filter((command) => command.value.startsWith(value) && command.value !== value && (!["/complete", "/cancel"].includes(command.value) || latestState.aggregateStatus === "open"));
36841
38102
  slashCompletionIndex = 0;
38103
+ }, invalidateAttachmentPreparation = function() {
38104
+ attachmentPreparationOwner = undefined;
38105
+ }, ownsAttachmentPreparation = function(owner) {
38106
+ return attachmentPreparationOwner === owner && !destroyed && !settled;
38107
+ }, viewIsActive = function() {
38108
+ return !destroyed && !settled;
36842
38109
  }, finish = function(result, error93) {
36843
38110
  if (settled)
36844
38111
  return;
36845
38112
  settled = true;
38113
+ invalidateAttachmentPreparation();
36846
38114
  if (error93)
36847
38115
  rejectAction(error93);
36848
38116
  else
@@ -36866,17 +38134,21 @@ async function createSessionTui({
36866
38134
  let settled = false;
36867
38135
  let activityExpanded = false;
36868
38136
  let composerDraft = initialComposer ?? { text: "", attachments: [] };
38137
+ let previousComposerText = composerDraft.text;
36869
38138
  let pathCompletions = [];
38139
+ let selectedPathCompletions = [];
36870
38140
  let completionIndex = 0;
36871
38141
  let pathCompletionMenuOpen = false;
36872
38142
  let slashCompletions = [];
36873
38143
  let slashCompletionIndex = 0;
36874
38144
  let composerFeedback = initialFeedback;
38145
+ let attachmentFeedback;
36875
38146
  const localPromptHistory = [];
36876
38147
  let promptHistoryIndex;
36877
38148
  let promptHistoryDraft = "";
36878
38149
  let historyReplacement;
36879
38150
  let pendingPastedImageWrites = 0;
38151
+ let attachmentPreparationOwner;
36880
38152
  let admittedSubmissions = [];
36881
38153
  let stopState = { kind: "idle" };
36882
38154
  let lifecycleRequestState = { kind: "idle" };
@@ -36915,6 +38187,12 @@ async function createSessionTui({
36915
38187
  submitComposer();
36916
38188
  },
36917
38189
  onContentChange: () => {
38190
+ selectedPathCompletions = reconcileSelectedPathCompletions({
38191
+ previousText: previousComposerText,
38192
+ text: composer.plainText,
38193
+ selections: selectedPathCompletions
38194
+ });
38195
+ previousComposerText = composer.plainText;
36918
38196
  if (historyReplacement !== composer.plainText)
36919
38197
  promptHistoryIndex = undefined;
36920
38198
  historyReplacement = undefined;
@@ -36995,12 +38273,12 @@ async function createSessionTui({
36995
38273
  };
36996
38274
  async function pasteClipboardImage() {
36997
38275
  if (!readClipboardImage || !savePastedImage) {
36998
- composerFeedback = "Clipboard image paste is unavailable.";
38276
+ attachmentFeedback = { kind: "failure", message: "Clipboard image paste is unavailable." };
36999
38277
  render();
37000
38278
  return;
37001
38279
  }
37002
38280
  pendingPastedImageWrites += 1;
37003
- composerFeedback = "Saving clipboard image\u2026";
38281
+ attachmentFeedback = { kind: "progress", message: "Saving clipboard image\u2026" };
37004
38282
  render();
37005
38283
  try {
37006
38284
  const image = await readClipboardImage();
@@ -37008,10 +38286,13 @@ async function createSessionTui({
37008
38286
  if (destroyed)
37009
38287
  return;
37010
38288
  composer.insertText(path);
37011
- composerFeedback = "Clipboard image added; send message to attach it.";
38289
+ const selectedCompletion = createSelectedPathCompletion({ attachmentPath: path, token: path, end: composer.cursorOffset });
38290
+ selectedPathCompletions = [...selectedPathCompletions, selectedCompletion];
38291
+ previousComposerText = composer.plainText;
38292
+ attachmentFeedback = { kind: "success", message: "Clipboard image ready to attach." };
37012
38293
  } catch (error93) {
37013
38294
  if (!destroyed)
37014
- composerFeedback = error93 instanceof Error ? error93.message : String(error93);
38295
+ attachmentFeedback = { kind: "failure", message: error93 instanceof Error ? error93.message : String(error93) };
37015
38296
  } finally {
37016
38297
  pendingPastedImageWrites -= 1;
37017
38298
  if (!destroyed)
@@ -37071,6 +38352,15 @@ async function createSessionTui({
37071
38352
  const completedText = replaceActivePathToken({ text: composer.plainText, replacement: completion.replacement });
37072
38353
  composer.setText(completedText);
37073
38354
  composer.cursorOffset = completedText.length;
38355
+ if (completion.attachmentPath) {
38356
+ const selectedCompletion = createSelectedPathCompletion({
38357
+ attachmentPath: completion.attachmentPath,
38358
+ token: completion.replacement,
38359
+ end: completedText.length
38360
+ });
38361
+ selectedPathCompletions = [...selectedPathCompletions, selectedCompletion];
38362
+ }
38363
+ previousComposerText = completedText;
37074
38364
  }
37075
38365
  pathCompletions = [];
37076
38366
  pathCompletionMenuOpen = false;
@@ -37101,8 +38391,10 @@ async function createSessionTui({
37101
38391
  render();
37102
38392
  }
37103
38393
  async function submitComposer() {
38394
+ if (attachmentPreparationOwner)
38395
+ return;
37104
38396
  if (pendingPastedImageWrites > 0) {
37105
- composerFeedback = "Saving pasted image\u2026";
38397
+ attachmentFeedback = { kind: "progress", message: "Saving pasted image\u2026" };
37106
38398
  render();
37107
38399
  return;
37108
38400
  }
@@ -37110,33 +38402,61 @@ async function createSessionTui({
37110
38402
  const value = composerDraft.text;
37111
38403
  if (value.trim() === "")
37112
38404
  return;
37113
- if (await handleComposerCommand(value))
38405
+ const preparationOwner = Symbol("attachment preparation");
38406
+ attachmentPreparationOwner = preparationOwner;
38407
+ if (await handleComposerCommand(value)) {
38408
+ if (attachmentPreparationOwner === preparationOwner)
38409
+ invalidateAttachmentPreparation();
38410
+ return;
38411
+ }
38412
+ if (!ownsAttachmentPreparation(preparationOwner))
37114
38413
  return;
37115
38414
  if (lifecycleRequestState.kind === "pending") {
37116
38415
  composerFeedback = `${lifecycleOperationPresentParticiple(lifecycleRequestState.operation)} the session \u2014 messages are paused.`;
37117
38416
  render();
38417
+ invalidateAttachmentPreparation();
37118
38418
  return;
37119
38419
  }
37120
38420
  if (latestState.aggregateStatus !== "open") {
37121
38421
  composerFeedback = "This session is already terminal. Run /sessions to return to the session list.";
37122
38422
  render();
38423
+ invalidateAttachmentPreparation();
37123
38424
  return;
37124
38425
  }
37125
38426
  if (attachPromptPaths) {
37126
- composerFeedback = "Attaching referenced files\u2026";
37127
- render();
38427
+ let handedOffToAdmission = false;
37128
38428
  try {
37129
38429
  const attachments = await attachPromptPaths({
37130
38430
  text: composerDraft.text,
37131
- excludedPaths: composerDraft.attachments.flatMap((attachment) => attachment.sourcePath ? [attachment.sourcePath] : [])
38431
+ excludedPaths: composerDraft.attachments.flatMap((attachment) => attachment.sourcePath ? [attachment.sourcePath] : []),
38432
+ selectedPaths: selectedAttachmentPaths({ text: composerDraft.text, selections: selectedPathCompletions }),
38433
+ onUploadStart: ({ filenames }) => {
38434
+ if (!ownsAttachmentPreparation(preparationOwner))
38435
+ return;
38436
+ attachmentFeedback = { kind: "progress", message: attachmentUploadMessage(filenames) };
38437
+ render();
38438
+ }
37132
38439
  });
38440
+ if (!ownsAttachmentPreparation(preparationOwner))
38441
+ return;
37133
38442
  composerDraft = { ...composerDraft, attachments: [...composerDraft.attachments, ...attachments] };
38443
+ selectedPathCompletions = [];
38444
+ attachmentFeedback = undefined;
38445
+ handedOffToAdmission = true;
37134
38446
  } catch (error93) {
37135
- composerFeedback = error93 instanceof Error ? error93.message : String(error93);
38447
+ if (!ownsAttachmentPreparation(preparationOwner))
38448
+ return;
38449
+ attachmentFeedback = { kind: "failure", message: error93 instanceof Error ? error93.message : String(error93) };
37136
38450
  render();
37137
38451
  return;
38452
+ } finally {
38453
+ if (!handedOffToAdmission && attachmentPreparationOwner === preparationOwner)
38454
+ invalidateAttachmentPreparation();
37138
38455
  }
37139
38456
  }
38457
+ if (!ownsAttachmentPreparation(preparationOwner))
38458
+ return;
38459
+ invalidateAttachmentPreparation();
37140
38460
  const submission = {
37141
38461
  id: crypto.randomUUID(),
37142
38462
  idempotencyKey: crypto.randomUUID(),
@@ -37173,13 +38493,13 @@ async function createSessionTui({
37173
38493
  if (command === "/complete") {
37174
38494
  composer.setText("");
37175
38495
  composerDraft = { ...composerDraft, text: "" };
37176
- await handleLifecycleRequest("complete");
38496
+ handleLifecycleRequest("complete");
37177
38497
  return true;
37178
38498
  }
37179
38499
  if (command === "/cancel") {
37180
38500
  composer.setText("");
37181
38501
  composerDraft = { ...composerDraft, text: "" };
37182
- await handleLifecycleRequest("cancel");
38502
+ handleLifecycleRequest("cancel");
37183
38503
  return true;
37184
38504
  }
37185
38505
  if (command === "/logout") {
@@ -37207,16 +38527,22 @@ async function createSessionTui({
37207
38527
  attachments: submission.attachments,
37208
38528
  idempotencyKey: submission.idempotencyKey
37209
38529
  });
38530
+ if (!viewIsActive())
38531
+ return;
37210
38532
  localPromptHistory.push(submission.text);
37211
38533
  const messageId = latestState.activeMessageId;
37212
38534
  admittedSubmissions = messageId ? admittedSubmissions.map((candidate) => candidate.id === submission.id ? { id: candidate.id, idempotencyKey: candidate.idempotencyKey, text: candidate.text, attachments: candidate.attachments, status: "submitted", messageId } : candidate) : admittedSubmissions.filter((candidate) => candidate.id !== submission.id);
37213
38535
  composerFeedback = undefined;
37214
38536
  } catch (error93) {
38537
+ if (!viewIsActive())
38538
+ return;
37215
38539
  admittedSubmissions = admittedSubmissions.map((candidate) => candidate.id === submission.id ? { id: candidate.id, idempotencyKey: candidate.idempotencyKey, text: candidate.text, attachments: candidate.attachments, status: "failed", error: error93 instanceof Error ? error93.message : String(error93) } : candidate);
37216
38540
  }
37217
38541
  render();
37218
38542
  }
37219
38543
  async function retryOldestFailedSubmission() {
38544
+ if (!viewIsActive())
38545
+ return;
37220
38546
  const failed = admittedSubmissions.find((submission) => submission.status === "failed");
37221
38547
  if (!failed)
37222
38548
  return;
@@ -37332,6 +38658,7 @@ async function createSessionTui({
37332
38658
  if (destroyed)
37333
38659
  return;
37334
38660
  destroyed = true;
38661
+ invalidateAttachmentPreparation();
37335
38662
  if (workingSpinnerTimer) {
37336
38663
  clearInterval(workingSpinnerTimer);
37337
38664
  workingSpinnerTimer = undefined;
@@ -37373,9 +38700,9 @@ function renderSlashCommandMenu({ commands, selectedIndex }) {
37373
38700
  const rows = commands.map((command, index) => {
37374
38701
  const selected = index === selectedIndex;
37375
38702
  const rowStyle = selected ? bg4(PALETTE.selectionBg) : (chunk) => chunk;
37376
- return new StyledText5([
37377
- rowStyle(fg6(PALETTE.remyAccent)(`${selected ? "\u203A " : " "}${command.value.padEnd(12)}`)),
37378
- rowStyle(fg6(PALETTE.dimText)(command.description))
38703
+ return new StyledText6([
38704
+ rowStyle(fg7(PALETTE.remyAccent)(`${selected ? "\u203A " : " "}${command.value.padEnd(12)}`)),
38705
+ rowStyle(fg7(PALETTE.dimText)(command.description))
37379
38706
  ]);
37380
38707
  });
37381
38708
  return joinStyled([joinStyled(rows, `
@@ -37396,15 +38723,15 @@ var helpEntries = [
37396
38723
  ];
37397
38724
  function renderHelpPanel() {
37398
38725
  const width = helpEntries.reduce((max, entry) => Math.max(max, entry.token.length), 0) + 2;
37399
- const rows = [new StyledText5([dim4(fg6(PALETTE.dimText)("What you can do here"))])];
38726
+ const rows = [new StyledText6([dim5(fg7(PALETTE.dimText)("What you can do here"))])];
37400
38727
  let lastGroup;
37401
38728
  for (const entry of helpEntries) {
37402
38729
  if (lastGroup !== undefined && entry.group !== lastGroup)
37403
- rows.push(new StyledText5([fg6(PALETTE.dimText)(" ")]));
38730
+ rows.push(new StyledText6([fg7(PALETTE.dimText)(" ")]));
37404
38731
  lastGroup = entry.group;
37405
- rows.push(new StyledText5([
37406
- fg6(PALETTE.remyAccent)(` ${entry.token.padEnd(width)}`),
37407
- dim4(fg6(PALETTE.dimText)(entry.description))
38732
+ rows.push(new StyledText6([
38733
+ fg7(PALETTE.remyAccent)(` ${entry.token.padEnd(width)}`),
38734
+ dim5(fg7(PALETTE.dimText)(entry.description))
37408
38735
  ]));
37409
38736
  }
37410
38737
  return joinStyled(rows, `
@@ -37418,23 +38745,22 @@ function renderStatusBand({ state, working, elapsedMs, stopState, lifecycleReque
37418
38745
  const pendingOperation = lifecycleRequestState?.kind === "pending" ? lifecycleRequestState.operation : undefined;
37419
38746
  const presentation = pendingOperation ? { label: lifecycleOperationPresentParticiple(pendingOperation), color: PALETTE.approvalQuestion } : stopping ? { label: "Stopping", color: PALETTE.approvalQuestion } : sessionViewStatePresentation({ aggregateStatus: state.aggregateStatus, isWorking: working });
37420
38747
  const elapsed = (working || stopping) && elapsedMs !== undefined && elapsedMs > 0 ? ` ${formatElapsed(elapsedMs)}` : "";
37421
- const connection = state.connectionStatus === "connected" ? dim4(fg6(PALETTE.dimText)("connected")) : fg6(PALETTE.approvalQuestion)("reconnecting\u2026");
37422
- const sessionLabel = state.sessionNumber !== undefined ? `Session #${state.sessionNumber}` : `Session ${state.sessionId}`;
37423
- const context = renderContextStatus(state.context.snapshot);
38748
+ const connection = state.connectionStatus === "connected" ? new StyledText6([fg7(PALETTE.statusCompleted)("\u25CF "), dim5(fg7(PALETTE.dimText)("connected"))]) : new StyledText6([fg7(PALETTE.approvalQuestion)("\u25CF reconnecting\u2026")]);
38749
+ const sessionLabel = state.sessionNumber !== undefined ? `#${state.sessionNumber}` : state.sessionId;
37424
38750
  const pullRequest = state.pullRequest;
37425
- return new StyledText5([
37426
- fg6(presentation.color)("\u25CF "),
37427
- ...repositoryLabel ? [dim4(fg6(PALETTE.dimText)(`${repositoryLabel} \xB7 `))] : [],
37428
- dim4(fg6(PALETTE.dimText)(`${sessionLabel} \xB7 `)),
37429
- bold4(fg6(presentation.color)(`${presentation.label}${elapsed}`)),
37430
- dim4(fg6(PALETTE.dimText)(" \xB7 ")),
37431
- connection,
37432
- ...context ? [dim4(fg6(PALETTE.dimText)(` \xB7 ${context}`))] : [],
37433
- ...pullRequest ? [
37434
- dim4(fg6(PALETTE.dimText)(" \xB7 ")),
37435
- bold4(fg6(statusColor(pullRequest.status))(`PR #${pullRequest.number} \xB7 ${pullRequest.draft ? "Draft" : sessionStatusLabel(pullRequest.status)}`))
37436
- ] : []
37437
- ]);
38751
+ const contextStatus = renderContextStatus(state.context.snapshot);
38752
+ const headerContext = [
38753
+ ...repositoryLabel ? [[dim5(fg7(PALETTE.dimText)(repositoryLabel))]] : [],
38754
+ ...pullRequest ? [[bold4(fg7(statusColor(pullRequest.status))(`PR#${pullRequest.number}`))]] : [],
38755
+ [bold4(fg7(presentation.color)(`${presentation.label}${elapsed}`))],
38756
+ connection.chunks,
38757
+ ...contextStatus ? [[dim5(fg7(PALETTE.dimText)(contextStatus))]] : []
38758
+ ];
38759
+ return joinStyled([
38760
+ [bold4(fg7(PALETTE.bodyText)(`${sessionLabel} ${state.title ?? "Untitled session"}`))],
38761
+ joinStyled(headerContext, " \xB7 ")
38762
+ ], `
38763
+ `);
37438
38764
  }
37439
38765
  function renderContextStatus(snapshot) {
37440
38766
  if (snapshot.status === "unknown")
@@ -37445,33 +38771,39 @@ function renderContextStatus(snapshot) {
37445
38771
  function renderTerminalSessionBand({ aggregateStatus }) {
37446
38772
  const status = sessionStatusLabel(aggregateStatus).toLowerCase();
37447
38773
  return joinStyled([
37448
- new StyledText5([
37449
- bg4(PALETTE.selectionBg)(bold4(fg6(PALETTE.systemAccent)(` Session ${status} `))),
37450
- bg4(PALETTE.selectionBg)(fg6(PALETTE.bodyText)("\u2014 this conversation is closed. "))
38774
+ new StyledText6([
38775
+ bg4(PALETTE.selectionBg)(bold4(fg7(PALETTE.systemAccent)(` Session ${status} `))),
38776
+ bg4(PALETTE.selectionBg)(fg7(PALETTE.bodyText)("\u2014 this conversation is closed. "))
37451
38777
  ]),
37452
- new StyledText5([
37453
- bg4(PALETTE.selectionBg)(fg6(PALETTE.remyAccent)(" esc: ")),
37454
- bg4(PALETTE.selectionBg)(dim4(fg6(PALETTE.dimText)("return to dashboard \xB7 "))),
37455
- bg4(PALETTE.selectionBg)(fg6(PALETTE.remyAccent)("n: ")),
37456
- bg4(PALETTE.selectionBg)(dim4(fg6(PALETTE.dimText)("new session ")))
38778
+ new StyledText6([
38779
+ bg4(PALETTE.selectionBg)(fg7(PALETTE.remyAccent)(" esc: ")),
38780
+ bg4(PALETTE.selectionBg)(dim5(fg7(PALETTE.dimText)("return to dashboard \xB7 "))),
38781
+ bg4(PALETTE.selectionBg)(fg7(PALETTE.remyAccent)("n: ")),
38782
+ bg4(PALETTE.selectionBg)(dim5(fg7(PALETTE.dimText)("new session ")))
37457
38783
  ])
37458
38784
  ], `
37459
38785
  `);
37460
38786
  }
37461
38787
  function renderActionBar({ state, working, stopState, lifecycleRequestState }) {
37462
38788
  if (lifecycleRequestState.kind === "pending")
37463
- return new StyledText5([dim4(fg6(PALETTE.dimText)(`${lifecycleOperationPresentParticiple(lifecycleRequestState.operation).toLowerCase()} the session\u2026`))]);
38789
+ return new StyledText6([dim5(fg7(PALETTE.dimText)(`${lifecycleOperationPresentParticiple(lifecycleRequestState.operation).toLowerCase()} the session\u2026`))]);
37464
38790
  if (state.aggregateStatus !== "open")
37465
- return new StyledText5([]);
38791
+ return new StyledText6([]);
37466
38792
  const stopToken = working && stopState.kind === "failed" ? ["esc retry stop"] : [];
37467
38793
  const idleGroup = lifecycleRequestState.kind === "failed" ? `/${lifecycleRequestState.operation} retries \xB7 /new \xB7 /sessions` : "/new \xB7 /sessions \xB7 /complete \xB7 /cancel";
37468
38794
  const tokens = working ? ["/ commands", ...stopToken, "ctrl+o activity"] : ["/ commands", idleGroup, "ctrl+o activity"];
37469
- return new StyledText5([dim4(fg6(PALETTE.dimText)(tokens.join(" \xB7 ")))]);
38795
+ return new StyledText6([dim5(fg7(PALETTE.dimText)(tokens.join(" \xB7 ")))]);
37470
38796
  }
37471
38797
  function isRemyWorking(state) {
37472
- if (state.aggregateStatus !== "open" || state.connectionStatus !== "connected")
38798
+ if (state.aggregateStatus !== "open" || state.connectionStatus !== "connected") {
37473
38799
  return false;
37474
- return hasRunningTurn(state) || state.activeMessageId !== undefined && state.messageTurns[state.activeMessageId]?.outcome === undefined;
38800
+ }
38801
+ if (state.activeMessageId !== undefined && state.messageTurns[state.activeMessageId]?.outcome === undefined)
38802
+ return true;
38803
+ return isWorkingAgentStatus(state.agentStatus) && hasRunningTurn(state);
38804
+ }
38805
+ function isWorkingAgentStatus(agentStatus) {
38806
+ return agentStatus === undefined || agentStatus === "pending" || agentStatus === "queued" || agentStatus === "running";
37475
38807
  }
37476
38808
  function workingTurnStartedAt(state) {
37477
38809
  const activeTurn = state.activeMessageId === undefined ? undefined : state.messageTurns[state.activeMessageId];
@@ -37512,58 +38844,64 @@ function renderTimeline({ state, activityExpanded }) {
37512
38844
  }
37513
38845
  function renderTimelineItem(item, activityExpanded) {
37514
38846
  if (item.kind === "message") {
37515
- const header = new StyledText5([
38847
+ const header = new StyledText6([
37516
38848
  renderTimestampChunk(item.occurredAt),
37517
38849
  renderRoleLabel({ label: item.author.label, role: item.author.role }),
37518
- ...item.author.source ? [dim4(fg6(PALETTE.dimText)(` (${item.author.source})`))] : []
38850
+ ...item.author.source ? [dim5(fg7(PALETTE.dimText)(` (${item.author.source})`))] : []
37519
38851
  ]);
37520
- return joinStyled([header, renderMessageBody({ text: item.text, role: item.author.role })], `
38852
+ const attachments = renderAttachmentSummary(item.attachments);
38853
+ return joinStyled([
38854
+ header,
38855
+ renderMessageBody({ text: item.text, role: item.author.role }),
38856
+ ...attachments.chunks.length > 0 ? [attachments] : []
38857
+ ], `
37521
38858
  `);
37522
38859
  }
37523
38860
  if (item.kind === "plan")
37524
38861
  return renderPlan({ item, expanded: activityExpanded });
37525
38862
  if (item.artifactKind === "tela_page") {
37526
- return new StyledText5([
38863
+ return new StyledText6([
37527
38864
  renderTimestampChunk(item.occurredAt),
37528
- bold4(fg6(PALETTE.tool)("Tela Page: ")),
37529
- fg6(PALETTE.bodyText)(item.title),
37530
- dim4(fg6(PALETTE.dimText)(` \xB7 ${item.url}`))
38865
+ bold4(fg7(PALETTE.tool)("Tela Page: ")),
38866
+ fg7(PALETTE.bodyText)(item.title),
38867
+ dim5(fg7(PALETTE.dimText)(` \xB7 ${item.url}`))
37531
38868
  ]);
37532
38869
  }
37533
- return new StyledText5([
38870
+ return new StyledText6([
37534
38871
  renderTimestampChunk(item.occurredAt),
37535
- bold4(fg6(PALETTE.tool)("Artifact: ")),
37536
- fg6(PALETTE.bodyText)(item.title)
38872
+ bold4(fg7(PALETTE.tool)("Artifact: ")),
38873
+ fg7(PALETTE.bodyText)(item.title),
38874
+ ...item.previewUrl ? [dim5(fg7(PALETTE.dimText)(` (${item.previewUrl})`))] : []
37537
38875
  ]);
37538
38876
  }
37539
38877
  var collapsedPlanItemLimit = 5;
37540
38878
  function renderPlan({ item, expanded }) {
37541
38879
  if (item.items.length === 0)
37542
- return new StyledText5([]);
38880
+ return new StyledText6([]);
37543
38881
  const completed = item.items.filter((workItem) => workItem.status === "completed").length;
37544
38882
  const visibleItems = expanded ? item.items : collapsedPlanItems(item.items);
37545
38883
  const rows = visibleItems.map((workItem) => {
37546
38884
  if (workItem.status === "in_progress") {
37547
- return new StyledText5([
37548
- fg6(PALETTE.remyAccent)(" \u25B8 "),
37549
- bold4(fg6(PALETTE.bodyText)(workItem.title))
38885
+ return new StyledText6([
38886
+ fg7(PALETTE.remyAccent)(" \u25B8 "),
38887
+ bold4(fg7(PALETTE.bodyText)(workItem.title))
37550
38888
  ]);
37551
38889
  }
37552
38890
  const glyph = workItem.status === "completed" ? "\u2713" : "\u25CB";
37553
- return new StyledText5([
37554
- fg6(PALETTE.dimText)(` ${glyph} `),
37555
- dim4(fg6(PALETTE.dimText)(workItem.title))
38891
+ return new StyledText6([
38892
+ fg7(PALETTE.dimText)(` ${glyph} `),
38893
+ dim5(fg7(PALETTE.dimText)(workItem.title))
37556
38894
  ]);
37557
38895
  });
37558
38896
  const hidden = item.items.length - visibleItems.length;
37559
38897
  return joinStyled([
37560
- new StyledText5([
38898
+ new StyledText6([
37561
38899
  renderTimestampChunk(item.occurredAt),
37562
- bold4(fg6(PALETTE.remyAccent)("Plan")),
37563
- dim4(fg6(PALETTE.dimText)(` \xB7 ${completed}/${item.items.length} done`))
38900
+ bold4(fg7(PALETTE.remyAccent)("Plan")),
38901
+ dim5(fg7(PALETTE.dimText)(` \xB7 ${completed}/${item.items.length} done`))
37564
38902
  ]),
37565
38903
  ...rows,
37566
- ...hidden > 0 ? [new StyledText5([dim4(fg6(PALETTE.dimText)(` \u2026 ${hidden} more task${hidden === 1 ? "" : "s"} \xB7 Ctrl+O details`))])] : []
38904
+ ...hidden > 0 ? [new StyledText6([dim5(fg7(PALETTE.dimText)(` \u2026 ${hidden} more task${hidden === 1 ? "" : "s"} \xB7 Ctrl+O details`))])] : []
37567
38905
  ], `
37568
38906
  `);
37569
38907
  }
@@ -37593,20 +38931,24 @@ function activityGlyph({ kind, inFlight }) {
37593
38931
  return { glyph: "\xB7", color: PALETTE.dimText };
37594
38932
  return { glyph: "\u2713", color: PALETTE.dimText };
37595
38933
  }
37596
- function dedupeActivitySteps(items) {
37597
- const runs = [];
37598
- for (const item of items) {
37599
- const last = runs[runs.length - 1];
37600
- if (last && last.item.card.kind === item.card.kind && last.item.card.title === item.card.title && last.item.card.summary === item.card.summary)
37601
- last.count += 1;
37602
- else
37603
- runs.push({ item, count: 1 });
38934
+ function activityCardsHaveEqualDisclosure(left, right) {
38935
+ return left.kind === right.kind && left.title === right.title && left.summary === right.summary && left.detail === right.detail && left.detailFormat === right.detailFormat && canonicalValuesEqual(left.attribution, right.attribution);
38936
+ }
38937
+ function canonicalValuesEqual(left, right) {
38938
+ if (Object.is(left, right))
38939
+ return true;
38940
+ if (Array.isArray(left) || Array.isArray(right)) {
38941
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => canonicalValuesEqual(value, right[index]));
37604
38942
  }
37605
- return runs;
38943
+ if (typeof left !== "object" || left === null || typeof right !== "object" || right === null)
38944
+ return false;
38945
+ const leftEntries = Object.entries(left);
38946
+ const rightEntries = Object.entries(right);
38947
+ return leftEntries.length === rightEntries.length && leftEntries.every(([key, value], index) => rightEntries[index]?.[0] === key && canonicalValuesEqual(value, rightEntries[index]?.[1]));
37606
38948
  }
37607
38949
  function renderActivityGroup({ items, activityExpanded }) {
37608
38950
  if (items.length === 0)
37609
- return new StyledText5([]);
38951
+ return new StyledText6([]);
37610
38952
  const summary = renderActivitySummary({ items, includeTimestamp: activityExpanded });
37611
38953
  const lastInFlightIndex = items.length - 1;
37612
38954
  if (!activityExpanded) {
@@ -37615,58 +38957,129 @@ function renderActivityGroup({ items, activityExpanded }) {
37615
38957
  item,
37616
38958
  inFlight: item.card.kind === "tool" && tailStart + index === lastInFlightIndex
37617
38959
  }));
37618
- const elision = tailStart > 0 ? new StyledText5([dim4(fg6(PALETTE.dimText)(` \u2026 ${tailStart} earlier step${tailStart === 1 ? "" : "s"}`))]) : undefined;
38960
+ const elision = tailStart > 0 ? new StyledText6([dim5(fg7(PALETTE.dimText)(` \u2026 ${tailStart} earlier step${tailStart === 1 ? "" : "s"}`))]) : undefined;
37619
38961
  return joinStyled([
37620
- new StyledText5([renderTimestampChunk(items[0].occurredAt), dim4(fg6(PALETTE.dimText)("Worked"))]),
38962
+ new StyledText6([renderTimestampChunk(items[0].occurredAt), dim5(fg7(PALETTE.dimText)("Worked"))]),
37621
38963
  ...elision ? [elision] : [],
37622
38964
  ...tail,
37623
38965
  summary
37624
38966
  ], `
37625
38967
  `);
37626
38968
  }
37627
- const steps = dedupeActivitySteps(items).map(({ item, count }) => {
37628
- const inFlight = item.card.kind === "tool" && items.indexOf(item) === lastInFlightIndex;
37629
- const { glyph, color } = activityGlyph({ kind: item.card.kind, inFlight });
37630
- const showSummary = item.card.summary.length > 0 && item.card.summary !== item.card.title && !boilerplateActivitySummaries.has(item.card.summary);
37631
- const detail = item.card.detail === undefined ? undefined : item.card.detailFormat === "code" ? renderCodeSnippet({ text: item.card.detail, indent: " " }) : new StyledText5([dim4(fg6(PALETTE.dimText)(` ${item.card.detail}`))]);
37632
- const step = new StyledText5([
37633
- fg6(color)(` ${glyph} `),
37634
- fg6(PALETTE.bodyText)(item.card.title),
37635
- ...count > 1 ? [dim4(fg6(PALETTE.dimText)(` \xD7${count}`))] : [],
37636
- ...showSummary ? [dim4(fg6(PALETTE.dimText)(`
37637
- ${item.card.summary}`))] : []
37638
- ]);
37639
- return detail ? joinStyled([step, detail], `
38969
+ return joinStyled([summary, ...renderExpandedActivityHierarchy({ items, lastInFlightIndex })], `
38970
+ `);
38971
+ }
38972
+ function renderExpandedActivityHierarchy({ items, lastInFlightIndex }) {
38973
+ const rendered = [];
38974
+ let activeOwnerKey;
38975
+ let index = 0;
38976
+ while (index < items.length) {
38977
+ const item = items[index];
38978
+ const attribution = item.card.attribution;
38979
+ if (attribution?.status === "resolved") {
38980
+ if (activeOwnerKey !== attribution.ownerKey) {
38981
+ rendered.push(new StyledText6([
38982
+ dim5(fg7(PALETTE.dimText)(" ")),
38983
+ fg7(PALETTE.bodyText)(`Subagent \xB7 ${lineagePathLabel(attribution.path)}`)
38984
+ ]));
38985
+ }
38986
+ activeOwnerKey = attribution.ownerKey;
38987
+ } else {
38988
+ activeOwnerKey = undefined;
38989
+ }
38990
+ let count = 1;
38991
+ while (items[index + count] && activityCardsHaveEqualDisclosure(item.card, items[index + count].card)) {
38992
+ count += 1;
38993
+ }
38994
+ rendered.push(renderExpandedActivityStep({
38995
+ item,
38996
+ count,
38997
+ inFlight: item.card.kind === "tool" && index === lastInFlightIndex,
38998
+ nested: attribution?.status === "resolved"
38999
+ }));
39000
+ index += count;
39001
+ }
39002
+ return rendered;
39003
+ }
39004
+ function renderExpandedActivityStep({ item, count, inFlight, nested }) {
39005
+ const { glyph, color } = activityGlyph({ kind: item.card.kind, inFlight });
39006
+ const showSummary = item.card.summary.length > 0 && item.card.summary !== item.card.title && !boilerplateActivitySummaries.has(item.card.summary);
39007
+ const disclosure = renderActivityDisclosure(item.card);
39008
+ const title = expandedActivityTitle(item.card);
39009
+ const indent = nested ? " " : " ";
39010
+ const step = new StyledText6([
39011
+ fg7(color)(`${indent}${glyph} `),
39012
+ fg7(PALETTE.bodyText)(title),
39013
+ ...count > 1 ? [dim5(fg7(PALETTE.dimText)(` \xD7${count}`))] : [],
39014
+ ...showSummary ? [dim5(fg7(PALETTE.dimText)(`
39015
+ ${indent} ${item.card.summary}`))] : []
39016
+ ]);
39017
+ return disclosure ? joinStyled([step, disclosure], `
37640
39018
  `) : step;
37641
- });
37642
- return joinStyled([summary, ...steps], `
39019
+ }
39020
+ function expandedActivityTitle(card) {
39021
+ if (!card.attribution)
39022
+ return card.title;
39023
+ if (card.attribution.status === "resolved")
39024
+ return card.attribution.activityTitle ?? "Activity";
39025
+ const name = childIdentityLabel(card.attribution.identity);
39026
+ return `Unattributed subagent \xB7 ${name}${card.attribution.activityTitle ? ` \xB7 ${card.attribution.activityTitle}` : ""}`;
39027
+ }
39028
+ function lineagePathLabel(path) {
39029
+ return path.map(childIdentityLabel).join(" \u203A ");
39030
+ }
39031
+ function childIdentityLabel(identity) {
39032
+ const safeName = terminalSafeText(identity.name ?? "").replace(/\s+/gu, " ").trim();
39033
+ if (safeName.length > 0)
39034
+ return safeName;
39035
+ return terminalSafeText(identity.subagentId).replace(/\s+/gu, " ").trim();
39036
+ }
39037
+ function renderActivityDisclosure(card) {
39038
+ const details = [];
39039
+ if (card.detail !== undefined) {
39040
+ const safeDetail = terminalSafeText(card.detail);
39041
+ details.push(card.detailFormat === "code" ? renderCodeSnippet({ text: safeDetail, indent: " " }) : new StyledText6([dim5(fg7(PALETTE.dimText)(` ${safeDetail}`))]));
39042
+ }
39043
+ return details.length === 0 ? undefined : joinStyled(details, `
39044
+
37643
39045
  `);
37644
39046
  }
37645
39047
  function renderActivitySummary({ items, includeTimestamp }) {
37646
- return new StyledText5([
39048
+ return new StyledText6([
37647
39049
  ...includeTimestamp ? [renderTimestampChunk(items[0].occurredAt)] : [],
37648
- dim4(fg6(PALETTE.dimText)(`Worked \xB7 ${items.length} step${items.length === 1 ? "" : "s"} \xB7 Ctrl+O details`))
39050
+ dim5(fg7(PALETTE.dimText)(`Worked \xB7 ${items.length} step${items.length === 1 ? "" : "s"} \xB7 Ctrl+O details`))
37649
39051
  ]);
37650
39052
  }
37651
39053
  function renderCollapsedActivityStep({ item, inFlight }) {
37652
39054
  const { glyph, color } = activityGlyph({ kind: item.card.kind, inFlight });
37653
- return new StyledText5([
37654
- fg6(color)(` ${glyph} `),
37655
- dim4(fg6(PALETTE.dimText)(item.card.title))
39055
+ return new StyledText6([
39056
+ fg7(color)(` ${glyph} `),
39057
+ dim5(fg7(PALETTE.dimText)(collapsedActivityTitle(item.card)))
37656
39058
  ]);
37657
39059
  }
39060
+ function collapsedActivityTitle(card) {
39061
+ const attribution = card.attribution;
39062
+ if (!attribution)
39063
+ return card.title;
39064
+ if (attribution.status === "unresolved") {
39065
+ const name = childIdentityLabel(attribution.identity);
39066
+ return `Unattributed subagent \xB7 ${name}${attribution.activityTitle ? ` \xB7 ${attribution.activityTitle}` : ""}`;
39067
+ }
39068
+ const activityTitle = attribution.activityTitle ? ` \xB7 ${attribution.activityTitle}` : "";
39069
+ return `${lineagePathLabel(attribution.path)}${activityTitle}`;
39070
+ }
37658
39071
  function renderSubmittedTimeline({ state, admittedSubmissions }) {
37659
39072
  const submitted = admittedSubmissions.filter((submission) => submission.status === "submitted" && !state.transcript.some((item) => item.kind === "message" && item.messageId === submission.messageId));
37660
39073
  const messages = submitted.map((submission) => {
37661
- const attachments = submission.attachments.length > 0 ? dim4(fg6(PALETTE.dimText)(`Attachments: ${submission.attachments.map((attachment) => attachment.filename).join(", ")}`)) : undefined;
37662
- const header = new StyledText5([
39074
+ const attachments = renderAttachmentSummary(submission.attachments);
39075
+ const header = new StyledText6([
37663
39076
  renderRoleLabel({ label: "You", role: "human" }),
37664
- dim4(fg6(PALETTE.dimText)(" (CLI)"))
39077
+ dim5(fg7(PALETTE.dimText)(" (CLI)"))
37665
39078
  ]);
37666
39079
  return joinStyled([
37667
39080
  header,
37668
39081
  renderMessageBody({ text: submission.text, role: "human" }),
37669
- ...attachments ? [new StyledText5([attachments])] : []
39082
+ ...attachments.chunks.length > 0 ? [attachments] : []
37670
39083
  ], `
37671
39084
  `);
37672
39085
  });
@@ -37676,20 +39089,20 @@ function renderSubmittedTimeline({ state, admittedSubmissions }) {
37676
39089
  }
37677
39090
  function renderWorkingIndicator({ frame, elapsedMs, mode }) {
37678
39091
  if (mode === "stopping") {
37679
- return new StyledText5([
37680
- fg6(PALETTE.approvalQuestion)("\u23F9"),
37681
- dim4(fg6(PALETTE.dimText)(` Stopping\u2026 ${formatElapsed(elapsedMs)}`))
39092
+ return new StyledText6([
39093
+ fg7(PALETTE.approvalQuestion)("\u23F9"),
39094
+ dim5(fg7(PALETTE.dimText)(` Stopping\u2026 ${formatElapsed(elapsedMs)}`))
37682
39095
  ]);
37683
39096
  }
37684
39097
  if (mode === "complete" || mode === "cancel") {
37685
- return new StyledText5([
37686
- fg6(PALETTE.approvalQuestion)(frame),
37687
- dim4(fg6(PALETTE.dimText)(` ${lifecycleOperationPresentParticiple(mode)} the session\u2026`))
39098
+ return new StyledText6([
39099
+ fg7(PALETTE.approvalQuestion)(frame),
39100
+ dim5(fg7(PALETTE.dimText)(` ${lifecycleOperationPresentParticiple(mode)} the session\u2026`))
37688
39101
  ]);
37689
39102
  }
37690
- return new StyledText5([
37691
- fg6(PALETTE.remyAccent)(frame),
37692
- dim4(fg6(PALETTE.dimText)(` Working ${formatElapsed(elapsedMs)}`))
39103
+ return new StyledText6([
39104
+ fg7(PALETTE.remyAccent)(frame),
39105
+ dim5(fg7(PALETTE.dimText)(` Working ${formatElapsed(elapsedMs)}`))
37693
39106
  ]);
37694
39107
  }
37695
39108
  function lifecycleOperationPresentParticiple(operation) {
@@ -37711,22 +39124,22 @@ function truncate2(value, width) {
37711
39124
  }
37712
39125
  function renderComposerStatus({ admittedSubmissions }) {
37713
39126
  if (admittedSubmissions.length === 0)
37714
- return new StyledText5([]);
39127
+ return new StyledText6([]);
37715
39128
  const parts = admittedSubmissions.flatMap((submission) => {
37716
39129
  if (submission.status === "submitted")
37717
39130
  return [];
37718
- const attachments = submission.attachments.length > 0 ? `
37719
- Attachments: ${submission.attachments.map((attachment) => attachment.filename).join(", ")}` : "";
37720
- return [submission.status === "failed" ? new StyledText5([
37721
- bold4(fg6(PALETTE.failure)("Failed: ")),
37722
- fg6(PALETTE.bodyText)(`${submission.error ?? ""}
37723
- ${submission.text}${attachments}
37724
- `),
37725
- dim4(fg6(PALETTE.dimText)("Ctrl+R retries oldest failed submission."))
37726
- ]) : new StyledText5([
37727
- fg6(PALETTE.progress)("Admitting: "),
37728
- fg6(PALETTE.bodyText)(`${submission.text}${attachments}`)
37729
- ])];
39131
+ const attachments = renderAttachmentSummary(submission.attachments);
39132
+ return [submission.status === "failed" ? joinStyled([
39133
+ new StyledText6([bold4(fg7(PALETTE.failure)("Failed: ")), fg7(PALETTE.bodyText)(submission.error ?? "")]),
39134
+ renderMessageBody({ text: submission.text, role: "human" }),
39135
+ ...attachments.chunks.length > 0 ? [attachments] : [],
39136
+ new StyledText6([dim5(fg7(PALETTE.dimText)("Ctrl+R retries oldest failed submission."))])
39137
+ ], `
39138
+ `) : joinStyled([
39139
+ new StyledText6([fg7(PALETTE.progress)("Admitting: "), fg7(PALETTE.bodyText)(submission.text)]),
39140
+ ...attachments.chunks.length > 0 ? [attachments] : []
39141
+ ], `
39142
+ `)];
37730
39143
  });
37731
39144
  return joinStyled(parts, `
37732
39145
 
@@ -37737,10 +39150,15 @@ async function createDefaultRenderer3() {
37737
39150
  }
37738
39151
 
37739
39152
  // src/tui/attachments.ts
39153
+ import { Buffer as Buffer2 } from "buffer";
37740
39154
  import { randomUUID as randomUUID4 } from "crypto";
37741
- import { writeFile as writeFile3, stat } from "fs/promises";
37742
- import { homedir as homedir2 } from "os";
39155
+ import { mkdtemp, open as open4, readdir as readdir2, rm, stat, writeFile as writeFile3 } from "fs/promises";
39156
+ import { homedir as homedir2, tmpdir } from "os";
37743
39157
  import { basename as basename3, isAbsolute as isAbsolute2, join as join5, resolve as resolve2 } from "path";
39158
+ var directoryArchiveMediaType = "application/gzip";
39159
+ var directoryArchiveSuffix = ".tar.gz";
39160
+ var maxPortableFilenameBytes = 255;
39161
+ var attachmentSizeLimitLabel = `${fileUploadMaxBytes / (1024 * 1024)} MiB`;
37744
39162
  async function readClipboardImage() {
37745
39163
  if (process.platform !== "darwin")
37746
39164
  throw new Error("Clipboard image paste is supported on macOS only.");
@@ -37766,11 +39184,13 @@ async function attachPromptPaths({
37766
39184
  text,
37767
39185
  cwd,
37768
39186
  excludedPaths = [],
37769
- reserveAndUploadFile: reserveAndUploadFile2
39187
+ selectedPaths = [],
39188
+ reserveAndUploadFile: reserveAndUploadFile2,
39189
+ onUploadStart
37770
39190
  }) {
37771
39191
  const excluded = new Set(excludedPaths);
37772
- const attachments = [];
37773
- for (const candidate of promptFilePaths({ text, cwd })) {
39192
+ const pathsToUpload = [];
39193
+ for (const candidate of promptFilePaths({ text, cwd, selectedPaths })) {
37774
39194
  const { filePath } = candidate;
37775
39195
  if (excluded.has(filePath))
37776
39196
  continue;
@@ -37782,27 +39202,168 @@ async function attachPromptPaths({
37782
39202
  continue;
37783
39203
  throw error93;
37784
39204
  }
37785
- if (!file3.isFile() && candidate.explicit)
37786
- throw new Error(`Prompt attachment path is not a regular file: ${filePath}`);
37787
- if (!file3.isFile())
39205
+ if (!file3.isFile() && !file3.isDirectory() && candidate.explicit)
39206
+ throw new Error(`Prompt attachment path is not a regular file or directory: ${filePath}`);
39207
+ if (!file3.isFile() && !file3.isDirectory())
37788
39208
  continue;
39209
+ pathsToUpload.push(filePath);
39210
+ }
39211
+ if (pathsToUpload.length === 0)
39212
+ return [];
39213
+ onUploadStart?.({ filenames: pathsToUpload.map((filePath) => basename3(filePath)) });
39214
+ const attachments = [];
39215
+ for (const filePath of pathsToUpload) {
39216
+ const uploaded = await uploadLocalAttachment({ filePath, reserveAndUploadFile: reserveAndUploadFile2 });
37789
39217
  attachments.push({
37790
39218
  id: randomUUID4(),
37791
- fileId: await reserveAndUploadFile2({ filePath }),
37792
- filename: basename3(filePath),
37793
- sourcePath: filePath
39219
+ ...uploaded
37794
39220
  });
37795
39221
  }
37796
39222
  return attachments;
37797
39223
  }
37798
- function promptFilePaths({ text, cwd }) {
39224
+ async function uploadLocalAttachment({
39225
+ filePath,
39226
+ reserveAndUploadFile: reserveAndUploadFile2
39227
+ }) {
39228
+ const sourcePath = resolve2(filePath);
39229
+ const source = await stat(sourcePath);
39230
+ if (source.isFile()) {
39231
+ if (source.size > fileUploadMaxBytes)
39232
+ throw oversizedFileAttachmentError(sourcePath);
39233
+ return {
39234
+ fileId: await reserveAndUploadFile2({ filePath: sourcePath }),
39235
+ filename: basename3(sourcePath),
39236
+ sourcePath
39237
+ };
39238
+ }
39239
+ if (!source.isDirectory())
39240
+ throw new Error(`Attachment path is not a regular file or directory: ${sourcePath}`);
39241
+ const archive = await createDirectoryArchive(sourcePath);
39242
+ try {
39243
+ return {
39244
+ fileId: await reserveAndUploadFile2({ filePath: archive.path, mediaType: directoryArchiveMediaType }),
39245
+ filename: archive.filename,
39246
+ sourcePath
39247
+ };
39248
+ } finally {
39249
+ await rm(archive.temporaryDirectory, { recursive: true, force: true });
39250
+ }
39251
+ }
39252
+ async function createDirectoryArchive(directoryPath) {
39253
+ const rootName = basename3(directoryPath) || "directory";
39254
+ const files = Object.create(null);
39255
+ await collectDirectoryFiles({
39256
+ directoryPath,
39257
+ archivePath: rootName,
39258
+ attachmentDirectoryPath: directoryPath,
39259
+ collectedByteSize: 0,
39260
+ files
39261
+ });
39262
+ if (Object.keys(files).length === 0)
39263
+ throw new Error(`Directory attachment contains no regular files: ${directoryPath}`);
39264
+ const temporaryDirectory = await mkdtemp(join5(tmpdir(), "remy-directory-attachment-"));
39265
+ const filename = directoryArchiveFilename(rootName);
39266
+ const archivePath = join5(temporaryDirectory, filename);
39267
+ try {
39268
+ await Bun.Archive.write(archivePath, files, { compress: "gzip" });
39269
+ if ((await stat(archivePath)).size > fileUploadMaxBytes)
39270
+ throw oversizedDirectoryArchiveError(directoryPath);
39271
+ } catch (error93) {
39272
+ await rm(temporaryDirectory, { recursive: true, force: true });
39273
+ throw error93;
39274
+ }
39275
+ return { path: archivePath, filename, temporaryDirectory };
39276
+ }
39277
+ async function collectDirectoryFiles({
39278
+ directoryPath,
39279
+ archivePath,
39280
+ attachmentDirectoryPath,
39281
+ collectedByteSize,
39282
+ files
39283
+ }) {
39284
+ const entries = await readdir2(directoryPath, { withFileTypes: true });
39285
+ entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
39286
+ let byteSize = collectedByteSize;
39287
+ for (const entry of entries) {
39288
+ const entryPath = join5(directoryPath, entry.name);
39289
+ const entryArchivePath = `${archivePath}/${entry.name}`;
39290
+ if (entry.isDirectory()) {
39291
+ byteSize = await collectDirectoryFiles({
39292
+ directoryPath: entryPath,
39293
+ archivePath: entryArchivePath,
39294
+ attachmentDirectoryPath,
39295
+ collectedByteSize: byteSize,
39296
+ files
39297
+ });
39298
+ continue;
39299
+ }
39300
+ if (entry.isFile()) {
39301
+ const fileHandle = await open4(entryPath, "r");
39302
+ try {
39303
+ const entryStat = await fileHandle.stat();
39304
+ if (!entryStat.isFile())
39305
+ throw new Error(`Directory attachment contains a non-regular entry: ${entryPath}`);
39306
+ const remainingBytes = fileUploadMaxBytes - byteSize;
39307
+ if (entryStat.size > remainingBytes)
39308
+ throw oversizedDirectoryAttachmentError(attachmentDirectoryPath);
39309
+ const readResult = await readFileHandleWithLimit({
39310
+ fileHandle,
39311
+ initialByteSize: entryStat.size,
39312
+ maxBytes: remainingBytes
39313
+ });
39314
+ if (readResult.status === "limit_exceeded")
39315
+ throw oversizedDirectoryAttachmentError(attachmentDirectoryPath);
39316
+ files[entryArchivePath] = readResult.bytes;
39317
+ byteSize += readResult.bytes.byteLength;
39318
+ } finally {
39319
+ await fileHandle.close();
39320
+ }
39321
+ continue;
39322
+ }
39323
+ if (entry.isSymbolicLink())
39324
+ throw new Error(`Directory attachment contains a symbolic link: ${entryPath}`);
39325
+ throw new Error(`Directory attachment contains a non-regular entry: ${entryPath}`);
39326
+ }
39327
+ return byteSize;
39328
+ }
39329
+ function oversizedFileAttachmentError(filePath) {
39330
+ return new Error(`Attachment file ${JSON.stringify(filePath)} is larger than ${attachmentSizeLimitLabel}. Choose a file no larger than ${attachmentSizeLimitLabel}.`);
39331
+ }
39332
+ function oversizedDirectoryAttachmentError(directoryPath) {
39333
+ return new Error(`Attachment directory ${JSON.stringify(directoryPath)} contains more than ${attachmentSizeLimitLabel} of files. Choose fewer or smaller files.`);
39334
+ }
39335
+ function oversizedDirectoryArchiveError(directoryPath) {
39336
+ return new Error(`Attachment directory ${JSON.stringify(directoryPath)} produces an archive larger than ${attachmentSizeLimitLabel}. Choose fewer or smaller files.`);
39337
+ }
39338
+ function directoryArchiveFilename(directoryName) {
39339
+ const stem = truncateUtf8(directoryName, maxPortableFilenameBytes - Buffer2.byteLength(directoryArchiveSuffix)) || "directory";
39340
+ return `${stem}${directoryArchiveSuffix}`;
39341
+ }
39342
+ function truncateUtf8(value, maxBytes) {
39343
+ let result = "";
39344
+ let resultBytes = 0;
39345
+ for (const character of value) {
39346
+ const characterBytes = Buffer2.byteLength(character);
39347
+ if (resultBytes + characterBytes > maxBytes)
39348
+ break;
39349
+ result += character;
39350
+ resultBytes += characterBytes;
39351
+ }
39352
+ return result;
39353
+ }
39354
+ function promptFilePaths({ text, cwd, selectedPaths }) {
37799
39355
  const paths = new Map;
39356
+ for (const selectedPath of selectedPaths)
39357
+ paths.set(isAbsolute2(selectedPath) ? selectedPath : resolve2(cwd, selectedPath), true);
37800
39358
  for (const rawToken of text.split(/\s+/)) {
37801
39359
  const token = rawToken.replace(/^[([{'"`]+/, "").replace(/[),.;:!?\]}"`]+$/, "");
37802
39360
  const pathText = token.startsWith("@") ? token.slice(1) : token;
37803
39361
  if (!pathText)
37804
39362
  continue;
37805
39363
  const explicit = token.startsWith("@") || pathText.startsWith("./") || pathText.startsWith("../") || isAbsolute2(pathText) || pathText.startsWith("~/");
39364
+ const pathShaped = explicit || pathText.includes("/") || pathText.includes("\\") || pathText.includes(".");
39365
+ if (!pathShaped)
39366
+ continue;
37806
39367
  const expandedPath = pathText.startsWith("~/") ? join5(homedir2(), pathText.slice(2)) : pathText;
37807
39368
  const filePath = isAbsolute2(expandedPath) ? expandedPath : resolve2(cwd, expandedPath);
37808
39369
  paths.set(filePath, paths.get(filePath) || explicit);
@@ -37825,7 +39386,7 @@ function isPng(bytes) {
37825
39386
  }
37826
39387
 
37827
39388
  // src/tui/remy-splash.ts
37828
- import { bold as bold5, BoxRenderable as BoxRenderable5, fg as fg7, NativeImage, StyledText as StyledText6, TextRenderable as TextRenderable5, bg as bg5 } from "@opentui/core";
39389
+ import { bold as bold5, BoxRenderable as BoxRenderable5, fg as fg8, NativeImage, StyledText as StyledText7, TextRenderable as TextRenderable5, bg as bg5 } from "@opentui/core";
37829
39390
  // src/tui/remy-mark.ts
37830
39391
  var remyPixelFieldSource = new URL("../assets/remy-pixel-field.png", import.meta.url);
37831
39392
  function shouldShowRemySplash({ width, height }) {
@@ -37844,7 +39405,7 @@ var compactMarkRows = 9;
37844
39405
  var compactMinWidth = 48;
37845
39406
  var compactMinHeight = 20;
37846
39407
  var markBrightnessGain = 4.2;
37847
- var remyCliVersion = "1.12.0";
39408
+ var remyCliVersion = "1.14.0";
37848
39409
  async function showRemySplash({
37849
39410
  createRenderer = createRemyRenderer,
37850
39411
  durationMs = splashDurationMs,
@@ -37902,7 +39463,7 @@ async function showRemySplash({
37902
39463
  });
37903
39464
  const approvalCopy = new TextRenderable5(renderer, { content: "" });
37904
39465
  const metadata = new TextRenderable5(renderer, {
37905
- content: new StyledText6([fg7(PALETTE.dimText)(`v${remyCliVersion} \xB7 Tela\xAE`)])
39466
+ content: new StyledText7([fg8(PALETTE.dimText)(`v${remyCliVersion} \xB7 Tela\xAE`)])
37906
39467
  });
37907
39468
  const field = await loadPixelFieldUntilAbort({ signal });
37908
39469
  if (!field || signal?.aborted)
@@ -38000,32 +39561,32 @@ function createSplashController({
38000
39561
  };
38001
39562
  }
38002
39563
  function renderBootstrapActions({ state, frame }) {
38003
- return new StyledText6([
39564
+ return new StyledText7([
38004
39565
  actionText({ label: state.authenticatedEmail ? `Authenticated as ${state.authenticatedEmail}` : "Checking authentication...", status: state.authentication, frame }),
38005
- fg7(PALETTE.dimText)(`
39566
+ fg8(PALETTE.dimText)(`
38006
39567
  `),
38007
39568
  actionText({ label: "Load sessions", status: state.loadingSessions, frame })
38008
39569
  ]);
38009
39570
  }
38010
39571
  function actionText({ label, status, frame }) {
38011
39572
  if (status === "complete")
38012
- return fg7(PALETTE.statusCompleted)(`\u2713 ${label === "Load sessions" ? "Sessions loaded!" : label}`);
39573
+ return fg8(PALETTE.statusCompleted)(`\u2713 ${label === "Load sessions" ? "Sessions loaded!" : label}`);
38013
39574
  if (status === "needed")
38014
- return fg7(PALETTE.approvalQuestion)("\u26A0 Authentication needed");
39575
+ return fg8(PALETTE.approvalQuestion)("\u26A0 Authentication needed");
38015
39576
  if (status === "active")
38016
- return fg7(PALETTE.progress)(`${splashSpinnerFrames[frame % splashSpinnerFrames.length]} ${label === "Load sessions" ? "Loading sessions..." : label}`);
38017
- return fg7(PALETTE.dimText)(`\u25CB ${label}`);
39577
+ return fg8(PALETTE.progress)(`${splashSpinnerFrames[frame % splashSpinnerFrames.length]} ${label === "Load sessions" ? "Loading sessions..." : label}`);
39578
+ return fg8(PALETTE.dimText)(`\u25CB ${label}`);
38018
39579
  }
38019
39580
  function renderAuthenticationBand({ state, frame }) {
38020
39581
  if (!state.waitingForAuthentication)
38021
- return new StyledText6([]);
39582
+ return new StyledText7([]);
38022
39583
  const spinner = splashSpinnerFrames[frame % splashSpinnerFrames.length];
38023
- const url3 = state.verificationUrl ? [fg7(PALETTE.dimText)(`
38024
- Verification URL: `), bold5(fg7(PALETTE.humanAccent)(state.verificationUrl))] : [];
38025
- return new StyledText6([
38026
- fg7(PALETTE.bodyText)("Continue authentication in your browser."),
39584
+ const url3 = state.verificationUrl ? [fg8(PALETTE.dimText)(`
39585
+ Verification URL: `), bold5(fg8(PALETTE.humanAccent)(state.verificationUrl))] : [];
39586
+ return new StyledText7([
39587
+ fg8(PALETTE.bodyText)("Continue authentication in your browser."),
38027
39588
  ...url3,
38028
- fg7(PALETTE.dimText)(`
39589
+ fg8(PALETTE.dimText)(`
38029
39590
  ${spinner} Waiting for approval \xB7 q / esc cancel`)
38030
39591
  ]);
38031
39592
  }
@@ -38100,7 +39661,7 @@ function renderMark(field, frame, rows = markRows) {
38100
39661
  const flush = () => {
38101
39662
  if (runLength === 0)
38102
39663
  return;
38103
- chunks.push(fg7(grey(runTop))(bg5(grey(runBottom))(upperHalfBlock.repeat(runLength))));
39664
+ chunks.push(fg8(grey(runTop))(bg5(grey(runBottom))(upperHalfBlock.repeat(runLength))));
38104
39665
  runLength = 0;
38105
39666
  };
38106
39667
  for (let column = 0;column < columns; column++) {
@@ -38117,10 +39678,10 @@ function renderMark(field, frame, rows = markRows) {
38117
39678
  }
38118
39679
  flush();
38119
39680
  if (row < rows - 1)
38120
- chunks.push(fg7("#000000")(`
39681
+ chunks.push(fg8("#000000")(`
38121
39682
  `));
38122
39683
  }
38123
- return new StyledText6(chunks);
39684
+ return new StyledText7(chunks);
38124
39685
  }
38125
39686
  function shimmerBrightness(field, column, sampleRow, columns, sampleRows, center) {
38126
39687
  const x0 = Math.floor(column / columns * field.width);
@@ -38140,8 +39701,8 @@ function grey(value) {
38140
39701
  return `#${hex5}${hex5}${hex5}`;
38141
39702
  }
38142
39703
  function renderSplashTitle() {
38143
- return new StyledText6([
38144
- bold5(fg7(PALETTE.bodyText)("Remy CLI"))
39704
+ return new StyledText7([
39705
+ bold5(fg8(PALETTE.bodyText)("Remy CLI"))
38145
39706
  ]);
38146
39707
  }
38147
39708
 
@@ -38476,6 +40037,9 @@ function createRepositoryLister({ client }) {
38476
40037
  function createRepositorySuggester({ client }) {
38477
40038
  return async (input) => await suggestRemoteRepositories({ client, input });
38478
40039
  }
40040
+ function createBranchSuggester({ client }) {
40041
+ return async (input) => await suggestRemoteBranches({ client, input });
40042
+ }
38479
40043
  function parseOptionalStringFlag(value, name) {
38480
40044
  if (value === undefined)
38481
40045
  return;
@@ -38619,48 +40183,98 @@ async function dashboard({
38619
40183
  const openDashboardTui = dependencies.openDashboardTui ?? createDashboardTui;
38620
40184
  const pages = [initialSessions];
38621
40185
  let pageIndex = 0;
38622
- let loadingPage;
40186
+ let filters = { statuses: [], creators: [] };
40187
+ let authors;
40188
+ let activeDashboardGeneration = 0;
38623
40189
  function holdTransitionScreen() {
38624
40190
  showRemyTransitionScreen({ write: (chunk) => dependencies.output.writeStdout(chunk) });
38625
40191
  }
38626
- async function loadDashboardPage({ target, signal }) {
38627
- if (loadingPage)
38628
- return await loadingPage;
38629
- loadingPage = (async () => {
38630
- if (target === "current") {
38631
- const after = pageIndex > 0 ? pages[pageIndex - 1]?.nextCursor ?? undefined : undefined;
38632
- const refreshed = toDashboardPage(await operations.listSessions({
40192
+ function createDashboardPageLoader({ generation }) {
40193
+ const loadingPages = new Map;
40194
+ let pageLoadTail = Promise.resolve();
40195
+ return async (input) => {
40196
+ const requestKey = "filters" in input ? `filters:${JSON.stringify(input.filters)}` : input.target;
40197
+ const matchingPageLoad = loadingPages.get(requestKey);
40198
+ if (matchingPageLoad)
40199
+ return await matchingPageLoad;
40200
+ const pendingPageLoad = pageLoadTail;
40201
+ const pageLoad = pendingPageLoad.then(async () => {
40202
+ if (generation !== activeDashboardGeneration)
40203
+ return { ...pages[pageIndex], canGoPrevious: pageIndex > 0 };
40204
+ if ("filters" in input) {
40205
+ const nextFilters = input.filters;
40206
+ const first = toDashboardPage(await operations.listSessions({
40207
+ client: operations.client,
40208
+ limit: 20,
40209
+ ...nextFilters.statuses.length > 0 ? { statuses: nextFilters.statuses } : {},
40210
+ ...nextFilters.creators.length > 0 ? { creatorUserIds: nextFilters.creators.map((creator) => creator.id) } : {},
40211
+ signal: input.signal
40212
+ }));
40213
+ if (generation !== activeDashboardGeneration)
40214
+ return first;
40215
+ filters = nextFilters;
40216
+ pages.splice(0, pages.length, first);
40217
+ pageIndex = 0;
40218
+ return first;
40219
+ }
40220
+ const { target, signal } = input;
40221
+ if (target === "current") {
40222
+ const after = pageIndex > 0 ? pages[pageIndex - 1]?.nextCursor ?? undefined : undefined;
40223
+ const refreshed = toDashboardPage(await operations.listSessions({
40224
+ client: operations.client,
40225
+ limit: 20,
40226
+ after,
40227
+ ...filters.statuses.length > 0 ? { statuses: filters.statuses } : {},
40228
+ ...filters.creators.length > 0 ? { creatorUserIds: filters.creators.map((creator) => creator.id) } : {},
40229
+ signal
40230
+ }));
40231
+ if (generation !== activeDashboardGeneration)
40232
+ return refreshed;
40233
+ pages.splice(pageIndex, pages.length - pageIndex, refreshed);
40234
+ return { ...refreshed, canGoPrevious: pageIndex > 0 };
40235
+ }
40236
+ if (target === "previous") {
40237
+ pageIndex = Math.max(0, pageIndex - 1);
40238
+ return { ...pages[pageIndex], canGoPrevious: pageIndex > 0 };
40239
+ }
40240
+ const current = pages[pageIndex];
40241
+ if (!current.hasMore || !current.nextCursor)
40242
+ return { ...current, canGoPrevious: pageIndex > 0 };
40243
+ const next = toDashboardPage(await operations.listSessions({
38633
40244
  client: operations.client,
38634
40245
  limit: 20,
38635
- after,
40246
+ after: current.nextCursor,
40247
+ ...filters.statuses.length > 0 ? { statuses: filters.statuses } : {},
40248
+ ...filters.creators.length > 0 ? { creatorUserIds: filters.creators.map((creator) => creator.id) } : {},
38636
40249
  signal
38637
40250
  }));
38638
- pages.splice(pageIndex, pages.length - pageIndex, refreshed);
38639
- return { ...refreshed, canGoPrevious: pageIndex > 0 };
38640
- }
38641
- if (target === "previous") {
38642
- pageIndex = Math.max(0, pageIndex - 1);
38643
- return { ...pages[pageIndex], canGoPrevious: pageIndex > 0 };
40251
+ if (generation !== activeDashboardGeneration)
40252
+ return next;
40253
+ pages.splice(pageIndex + 1);
40254
+ pages.push(next);
40255
+ pageIndex += 1;
40256
+ return { ...next, canGoPrevious: pageIndex > 0 };
40257
+ });
40258
+ loadingPages.set(requestKey, pageLoad);
40259
+ pageLoadTail = pageLoad.then(() => {
40260
+ return;
40261
+ }, () => {
40262
+ return;
40263
+ });
40264
+ try {
40265
+ return await pageLoad;
40266
+ } finally {
40267
+ if (loadingPages.get(requestKey) === pageLoad)
40268
+ loadingPages.delete(requestKey);
38644
40269
  }
38645
- const current = pages[pageIndex];
38646
- if (!current.hasMore || !current.nextCursor)
38647
- return { ...current, canGoPrevious: pageIndex > 0 };
38648
- const next = toDashboardPage(await operations.listSessions({
38649
- client: operations.client,
38650
- limit: 20,
38651
- after: current.nextCursor,
38652
- signal
38653
- }));
38654
- pages.splice(pageIndex + 1);
38655
- pages.push(next);
38656
- pageIndex += 1;
38657
- return { ...next, canGoPrevious: pageIndex > 0 };
38658
- })();
38659
- try {
38660
- return await loadingPage;
38661
- } finally {
38662
- loadingPage = undefined;
38663
- }
40270
+ };
40271
+ }
40272
+ async function loadDashboardAuthors() {
40273
+ if (authors)
40274
+ return authors;
40275
+ const loadedAuthors = await collectAllUsers({ listUsers: operations.listUsers, signal: dependencies.abortSignal });
40276
+ authors = loadedAuthors;
40277
+ return loadedAuthors;
38664
40278
  }
38665
40279
  let firstOpen = true;
38666
40280
  holdTransitionScreen();
@@ -38669,17 +40283,30 @@ async function dashboard({
38669
40283
  pages.splice(0, pages.length, toDashboardPage(await operations.listSessions({
38670
40284
  client: operations.client,
38671
40285
  limit: 20,
40286
+ ...filters.statuses.length > 0 ? { statuses: filters.statuses } : {},
40287
+ ...filters.creators.length > 0 ? { creatorUserIds: filters.creators.map((creator) => creator.id) } : {},
38672
40288
  signal: dependencies.abortSignal
38673
40289
  })));
38674
40290
  pageIndex = 0;
38675
40291
  }
38676
40292
  firstOpen = false;
40293
+ const dashboardGeneration = ++activeDashboardGeneration;
40294
+ const loadDashboardPage = createDashboardPageLoader({ generation: dashboardGeneration });
38677
40295
  const action = await renderRemyView({
38678
40296
  open: async () => await openDashboardTui({
38679
40297
  initialPage: pages[0],
40298
+ initialFilters: filters,
40299
+ loadAuthors: loadDashboardAuthors,
38680
40300
  loadPage: loadDashboardPage
38681
40301
  }),
38682
- waitForAction: async (dashboardTui) => await awaitWithAbort({ value: dashboardTui.waitForAction(), abortSignal: dependencies.abortSignal })
40302
+ waitForAction: async (dashboardTui) => {
40303
+ try {
40304
+ return await awaitWithAbort({ value: dashboardTui.waitForAction(), abortSignal: dependencies.abortSignal });
40305
+ } finally {
40306
+ if (activeDashboardGeneration === dashboardGeneration)
40307
+ activeDashboardGeneration += 1;
40308
+ }
40309
+ }
38683
40310
  });
38684
40311
  if (!action)
38685
40312
  return 0;
@@ -38722,12 +40349,12 @@ async function prepareDashboardBootstrap({
38722
40349
  return;
38723
40350
  });
38724
40351
  onLoadingSessions?.();
38725
- const initialSessions = toDashboardPage(await resolvedOperations.listSessions({
40352
+ const initialSessionPage = await resolvedOperations.listSessions({
38726
40353
  client: resolvedOperations.client,
38727
40354
  limit: 20,
38728
40355
  signal: dependencies.abortSignal
38729
- }));
38730
- return { operations: resolvedOperations, repositories, initialSessions };
40356
+ });
40357
+ return { operations: resolvedOperations, repositories, initialSessions: toDashboardPage(initialSessionPage) };
38731
40358
  }
38732
40359
  async function prepareDashboardWithStartupSplash({
38733
40360
  dependencies,
@@ -38918,11 +40545,15 @@ async function createNewSession({
38918
40545
  throw new Error("All selected repositories must belong to the same GitHub installation.");
38919
40546
  const fileIds = [];
38920
40547
  for (const attachment of command.attachments) {
38921
- fileIds.push(await operations.reserveAndUploadFile({
38922
- client: operations.client,
40548
+ const uploaded = await uploadLocalAttachment({
38923
40549
  filePath: attachment,
38924
- idempotencyKey: randomUUID5()
38925
- }));
40550
+ reserveAndUploadFile: async (input) => await operations.reserveAndUploadFile({
40551
+ ...input,
40552
+ client: operations.client,
40553
+ idempotencyKey: randomUUID5()
40554
+ })
40555
+ });
40556
+ fileIds.push(uploaded.fileId);
38926
40557
  }
38927
40558
  const created = await operations.createCodexSession({
38928
40559
  client: operations.client,
@@ -38996,6 +40627,7 @@ async function createFromTuiDraft({
38996
40627
  input: {
38997
40628
  installationId: draft.githubInstallationId,
38998
40629
  repositoryIds: draft.repositories.map((repository) => repository.id),
40630
+ repositoryBranchOverrides: draft.repositoryBranchOverrides ?? [],
38999
40631
  prompt: draft.prompt,
39000
40632
  fileIds: draft.attachments.map((attachment) => attachment.fileId),
39001
40633
  model: draft.model,
@@ -39018,10 +40650,11 @@ async function openTuiNewSessionWizard({
39018
40650
  repositories,
39019
40651
  reloadRepositories,
39020
40652
  suggestRepositories: operations.suggestRepositories,
40653
+ suggestBranches: operations.suggestBranches,
39021
40654
  initialDraft,
39022
40655
  initialError,
39023
40656
  readClipboardImage,
39024
- savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir() }),
40657
+ savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir2() }),
39025
40658
  attachPromptPaths: async (input) => await uploadPromptPaths({ operations, ...input })
39026
40659
  }),
39027
40660
  waitForAction: async (wizard) => await awaitWithAbort({ value: wizard.waitForDraft(), abortSignal: dependencies.abortSignal })
@@ -39030,15 +40663,20 @@ async function openTuiNewSessionWizard({
39030
40663
  async function uploadPromptPaths({
39031
40664
  operations,
39032
40665
  text,
39033
- excludedPaths
40666
+ excludedPaths,
40667
+ selectedPaths,
40668
+ onUploadStart
39034
40669
  }) {
39035
40670
  return await attachPromptPaths({
39036
40671
  text,
39037
40672
  excludedPaths,
40673
+ selectedPaths,
40674
+ onUploadStart,
39038
40675
  cwd: process.cwd(),
39039
- reserveAndUploadFile: async ({ filePath }) => await operations.reserveAndUploadFile({
40676
+ reserveAndUploadFile: async ({ filePath, mediaType }) => await operations.reserveAndUploadFile({
39040
40677
  client: operations.client,
39041
40678
  filePath,
40679
+ ...mediaType ? { mediaType } : {},
39042
40680
  idempotencyKey: randomUUID5()
39043
40681
  })
39044
40682
  });
@@ -39096,7 +40734,7 @@ async function runAttachedSession({
39096
40734
  ...activeMessageId ? { activeMessageId } : {},
39097
40735
  environment: dependencies.environment,
39098
40736
  getSession: async ({ sessionId: id }) => await operations.getSession({ client: operations.client, sessionId: id }),
39099
- listSessionEvents: async ({ sessionId: id, limit, after }) => await operations.listSessionEvents({ client: operations.client, sessionId: id, limit, after }),
40737
+ listSessionEvents: async ({ sessionId: id, limit, after, signal }) => await operations.listSessionEvents({ client: operations.client, sessionId: id, limit, after, signal }),
39100
40738
  openEventStream: ({ sessionId: id, lastRetainedEventId, signal, onSynchronized }) => operations.streamSessionEvents({ client: operations.client, sessionId: id, lastRetainedEventId, signal, onSynchronized })
39101
40739
  });
39102
40740
  const interactive = !noTui && !json3 && isInteractiveTerminal(dependencies);
@@ -39135,8 +40773,14 @@ async function runAttachedSession({
39135
40773
  });
39136
40774
  }
39137
40775
  const startPromise = controller.start(start);
39138
- await Promise.race([controller.waitUntilReady(), startPromise]);
40776
+ await Promise.race([
40777
+ controller.waitUntilReady(),
40778
+ startPromise,
40779
+ ...aborted3 ? [aborted3] : []
40780
+ ]);
39139
40781
  if (interactive) {
40782
+ if (dependencies.abortSignal?.aborted)
40783
+ throw dependencies.abortSignal.reason ?? new Error("interrupted");
39140
40784
  const openSessionTui = dependencies.openSessionTui ?? createSessionTui;
39141
40785
  let composerSnapshot;
39142
40786
  const holdTransitionScreen = () => showRemyTransitionScreen({ write: (chunk) => dependencies.output.writeStdout(chunk) });
@@ -39173,7 +40817,7 @@ async function runAttachedSession({
39173
40817
  controller.updateDetail(detail);
39174
40818
  },
39175
40819
  readClipboardImage,
39176
- savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir() }),
40820
+ savePastedImage: async (input) => await writePastedImage({ ...input, temporaryDirectory: tmpdir2() }),
39177
40821
  attachPromptPaths: async (input) => await uploadPromptPaths({ operations, ...input })
39178
40822
  }),
39179
40823
  waitForAction: async (tui) => await Promise.race([tui.waitForAction(), aborted3])
@@ -39282,7 +40926,9 @@ async function createSessionOperations(dependencies, { onAuthenticated } = {}) {
39282
40926
  return {
39283
40927
  client: dependencies.sessionClient,
39284
40928
  listRepositories: dependencies.listRepositories ?? (async () => await Promise.reject(new Error("Missing remote repository operation."))),
40929
+ listUsers: dependencies.listUsers ?? (async (input) => await listRemoteUsers({ client: dependencies.sessionClient, ...input })),
39285
40930
  suggestRepositories: dependencies.suggestRepositories ?? (async () => await Promise.reject(new Error("Missing remote repository suggestions operation."))),
40931
+ suggestBranches: dependencies.suggestBranches ?? (async () => await Promise.reject(new Error("Missing remote branch suggestions operation."))),
39286
40932
  reserveAndUploadFile: dependencies.reserveAndUploadFile ?? reserveAndUploadFile,
39287
40933
  createCodexSession: dependencies.createCodexSession ?? createCodexSession,
39288
40934
  appendSessionMessage: dependencies.appendSessionMessage ?? appendSessionMessage,
@@ -39300,7 +40946,9 @@ async function createSessionOperations(dependencies, { onAuthenticated } = {}) {
39300
40946
  return {
39301
40947
  client,
39302
40948
  listRepositories: dependencies.listRepositories ?? createRepositoryLister({ client }),
40949
+ listUsers: dependencies.listUsers ?? (async (input) => await listRemoteUsers({ client, ...input })),
39303
40950
  suggestRepositories: dependencies.suggestRepositories ?? createRepositorySuggester({ client }),
40951
+ suggestBranches: dependencies.suggestBranches ?? createBranchSuggester({ client }),
39304
40952
  reserveAndUploadFile: dependencies.reserveAndUploadFile ?? reserveAndUploadFile,
39305
40953
  createCodexSession: dependencies.createCodexSession ?? createCodexSession,
39306
40954
  appendSessionMessage: dependencies.appendSessionMessage ?? appendSessionMessage,
@@ -39342,6 +40990,20 @@ async function collectAllRepositories({
39342
40990
  after = page.nextCursor;
39343
40991
  }
39344
40992
  }
40993
+ async function collectAllUsers({
40994
+ listUsers,
40995
+ signal
40996
+ }) {
40997
+ const users = [];
40998
+ let after;
40999
+ while (true) {
41000
+ const page = await listUsers({ limit: 100, after, signal });
41001
+ users.push(...page.data);
41002
+ if (!page.hasMore || !page.nextCursor)
41003
+ return users;
41004
+ after = page.nextCursor;
41005
+ }
41006
+ }
39345
41007
  function resolveSessionCachePathForCommand({ dependencies, sessionId }) {
39346
41008
  const stateBase = dependencies.environment.XDG_STATE_HOME ?? (dependencies.environment.HOME ? join6(dependencies.environment.HOME, ".local/state") : undefined);
39347
41009
  if (!stateBase)
@@ -39468,7 +41130,7 @@ Options:
39468
41130
  --installation <id> Select a GitHub installation
39469
41131
  --model <name> Select an agent model
39470
41132
  --reasoning-effort <low|medium|high|xhigh> Select agent reasoning effort
39471
- --attach <path> Upload a file; repeat for more attachments
41133
+ --attach <path> Upload a file or directory (20 MiB max); repeat as needed
39472
41134
  --no-tui Do not open the interactive terminal view
39473
41135
  --json Write session updates as JSON
39474
41136
  --help, -h Show this help
@@ -39617,7 +41279,7 @@ async function removeCommittedTokenRecoveryArtifacts({
39617
41279
  const directory = dirname6(tokenPath);
39618
41280
  let entries;
39619
41281
  try {
39620
- entries = await readdir2(directory);
41282
+ entries = await readdir3(directory);
39621
41283
  } catch (error93) {
39622
41284
  if (isMissingFileError3(error93))
39623
41285
  return;