negotium 0.22.7 → 0.23.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.
package/dist/main.js CHANGED
@@ -6559,7 +6559,7 @@ var exports_version = {};
6559
6559
  __export(exports_version, {
6560
6560
  NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
6561
6561
  });
6562
- var NEGOTIUM_VERSION = "0.22.7";
6562
+ var NEGOTIUM_VERSION = "0.23.0";
6563
6563
 
6564
6564
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
6565
6565
  import { spawn as spawn2 } from "child_process";
@@ -21485,7 +21485,7 @@ function buildVideoHtml(mimeType) {
21485
21485
  <body><video controls playsinline preload="metadata" src="${VISUAL_MEDIA_URL_PLACEHOLDER}" data-mime="${escapeHtml2(mimeType)}"></video></body>
21486
21486
  </html>`;
21487
21487
  }
21488
- function resolveVisualMediaInput(topicId, input) {
21488
+ function locateVisualMediaInput(topicId, input) {
21489
21489
  if (typeof input.file_id === "string" && input.file_id.trim()) {
21490
21490
  const fileId = input.file_id.trim().toLowerCase();
21491
21491
  if (!isUploadFileId(fileId))
@@ -21493,15 +21493,11 @@ function resolveVisualMediaInput(topicId, input) {
21493
21493
  if (!topicAllowsVisualFileId(topicId, fileId)) {
21494
21494
  return { error: "file_id is not attached to this topic" };
21495
21495
  }
21496
- const attachment2 = resolveAttachmentByFileId(fileId);
21497
- const filePath2 = resolveUploadedFilePathByFileId(fileId);
21498
- if (!attachment2 || !filePath2)
21496
+ const attachment = resolveAttachmentByFileId(fileId);
21497
+ const filePath = resolveUploadedFilePathByFileId(fileId);
21498
+ if (!attachment || !filePath)
21499
21499
  return { error: `uploaded file not found: ${fileId}` };
21500
- return {
21501
- fileId: attachment2.id,
21502
- mimeType: attachment2.mimeType,
21503
- source: `file_id:${attachment2.id}`
21504
- };
21500
+ return { kind: "upload", fileId: attachment.id, mimeType: attachment.mimeType };
21505
21501
  }
21506
21502
  if (typeof input.file_path !== "string" || !input.file_path.trim()) {
21507
21503
  return { error: "file_path or file_id is required" };
@@ -21509,18 +21505,38 @@ function resolveVisualMediaInput(topicId, input) {
21509
21505
  const rawPath = input.file_path.trim();
21510
21506
  const cwd = workspaceCwdFor(topicId);
21511
21507
  const candidate = isAbsolute5(rawPath) ? rawPath : resolve16(cwd, rawPath);
21512
- if (!isPathInside(cwd, candidate)) {
21513
- return { error: "file_path must be inside the topic workspace" };
21514
- }
21515
21508
  let realCandidate;
21516
21509
  try {
21517
21510
  realCandidate = realpathSync5(candidate);
21518
21511
  } catch {
21519
21512
  return { error: `file not found: ${rawPath}` };
21520
21513
  }
21514
+ if (!isPathInside(cwd, realCandidate)) {
21515
+ return {
21516
+ error: `file_path must be inside the topic workspace (${cwd}); copy the file into it and pass the copy`
21517
+ };
21518
+ }
21521
21519
  if (isSensitivePath(realCandidate)) {
21522
21520
  return { error: "file_path matches the sensitive-file blacklist" };
21523
21521
  }
21522
+ return { kind: "file", path: realCandidate, rawPath };
21523
+ }
21524
+ function visualMediaInputError(topicId, input) {
21525
+ const located = locateVisualMediaInput(topicId, input);
21526
+ return "error" in located ? located.error : null;
21527
+ }
21528
+ function resolveVisualMediaInput(topicId, input) {
21529
+ const located = locateVisualMediaInput(topicId, input);
21530
+ if ("error" in located)
21531
+ return located;
21532
+ if (located.kind === "upload") {
21533
+ return {
21534
+ fileId: located.fileId,
21535
+ mimeType: located.mimeType,
21536
+ source: `file_id:${located.fileId}`
21537
+ };
21538
+ }
21539
+ const { path: realCandidate, rawPath } = located;
21524
21540
  const attachment = storeLocalFileAsUpload(realCandidate, { topicId });
21525
21541
  if (!attachment)
21526
21542
  return { error: `failed to store media file: ${rawPath}` };
@@ -22791,6 +22807,7 @@ __export(exports_turn_runner, {
22791
22807
  workspaceCwdFor: () => workspaceCwdFor,
22792
22808
  withDefaultPlaywright: () => withDefaultPlaywright,
22793
22809
  wasLocallyRequeuedAfterUserPreemption: () => wasLocallyRequeuedAfterUserPreemption,
22810
+ visualMediaInputError: () => visualMediaInputError,
22794
22811
  userConversationPromptsToRecord: () => userConversationPromptsToRecord,
22795
22812
  upsertTaskPanelMessage: () => upsertTaskPanelMessage,
22796
22813
  tryReconstructTopicRollout: () => tryReconstructTopicRollout,
@@ -30444,6 +30461,7 @@ var init_thread_read = __esm(async () => {
30444
30461
  var exports_mcp_runtime_host = {};
30445
30462
  __export(exports_mcp_runtime_host, {
30446
30463
  visualToolDefinitions: () => visualToolDefinitions,
30464
+ visualMediaInputError: () => visualMediaInputError,
30447
30465
  textResult: () => textResult,
30448
30466
  storeLocalFileAsUpload: () => storeLocalFileAsUpload,
30449
30467
  showPngTool: () => showPngTool,
@@ -30457,6 +30475,7 @@ __export(exports_mcp_runtime_host, {
30457
30475
  prepareDeliveryAck: () => prepareDeliveryAck,
30458
30476
  logger: () => logger,
30459
30477
  listTopicThreadRoots: () => listTopicThreadRoots,
30478
+ isVisualsShowImageTool: () => isVisualsShowImageTool,
30460
30479
  isSensitivePath: () => isSensitivePath,
30461
30480
  isParticipant: () => isParticipant,
30462
30481
  isInsideDir: () => isInsideDir,
@@ -30533,6 +30552,7 @@ var init_mcp_runtime_host = __esm(async () => {
30533
30552
  init_actor_topic_reach(),
30534
30553
  init_delivery_ack(),
30535
30554
  init_thread_read(),
30555
+ init_visuals(),
30536
30556
  init_api_messages(),
30537
30557
  init_api_topic_config(),
30538
30558
  init_api_topics(),
@@ -35524,7 +35544,12 @@ function buildNegotiumMcpServer(ctx) {
35524
35544
  const server = new McpServer8({ name: RUNTIME_MCP_KEY, version: "1.0.0" });
35525
35545
  if (ctx.visualTools === true) {
35526
35546
  for (const def of [...visualToolDefinitions, showPngTool]) {
35527
- const handler = ctx.peerBridge && (def.name === "show_html" || def.name === "show_mermaid") ? async () => textResult("Visual queued for ordered display on the canonical hub.") : def.handler;
35547
+ const handler = ctx.peerBridge && (def.name === "show_html" || def.name === "show_mermaid") ? async () => textResult("Visual queued for ordered display on the canonical hub.") : isVisualsShowImageTool(def.name) ? async (input) => {
35548
+ const problem = visualMediaInputError(ctx.topicId, input ?? {});
35549
+ if (problem)
35550
+ return errorResult(`Error: ${problem}`);
35551
+ return def.handler(input);
35552
+ } : def.handler;
35528
35553
  server.tool(def.name, def.description, def.schema, handler);
35529
35554
  }
35530
35555
  const publishTools = createPublishHtmlToolDefinitions({ cwd: ctx.cwd });
@@ -41904,7 +41929,10 @@ function createInitialState(userId) {
41904
41929
  decisionGraphOffset: { x: 0, y: 0 },
41905
41930
  decisionGraphSpacing: DEFAULT_DECISION_GRAPH_SPACING,
41906
41931
  askChoiceIndex: 0,
41907
- taskSidebarEnabled: true,
41932
+ taskPanelScrollOffset: 0,
41933
+ topicsPanelEnabled: true,
41934
+ topicsPanelScrollOffset: 0,
41935
+ topicsPanelSort: "activity",
41908
41936
  overlay: null,
41909
41937
  topicPickerRoot: false,
41910
41938
  vaultEntries: [],
@@ -41916,8 +41944,27 @@ function createInitialState(userId) {
41916
41944
  vaultRequestSubmitting: false
41917
41945
  };
41918
41946
  }
41919
- function toggleTaskSidebar(state) {
41920
- return { ...state, taskSidebarEnabled: !state.taskSidebarEnabled };
41947
+ function toggleTopicsPanel(state) {
41948
+ return { ...state, topicsPanelEnabled: !state.topicsPanelEnabled };
41949
+ }
41950
+ function cycleTopicsPanelSort(state) {
41951
+ return { ...state, topicsPanelSort: state.topicsPanelSort === "activity" ? "name" : "activity" };
41952
+ }
41953
+ function scrollTopicsPanel(state, delta, maxOffset) {
41954
+ const next = Math.min(maxOffset, Math.max(0, state.topicsPanelScrollOffset + delta));
41955
+ return next === state.topicsPanelScrollOffset ? state : { ...state, topicsPanelScrollOffset: next };
41956
+ }
41957
+ function scrollTaskPanel(state, delta, maxOffset) {
41958
+ const next = Math.min(maxOffset, Math.max(0, state.taskPanelScrollOffset + delta));
41959
+ return next === state.taskPanelScrollOffset ? state : { ...state, taskPanelScrollOffset: next };
41960
+ }
41961
+ function setTopicsPanelScrollOffset(state, offset, maxOffset) {
41962
+ const next = Math.min(maxOffset, Math.max(0, offset));
41963
+ return next === state.topicsPanelScrollOffset ? state : { ...state, topicsPanelScrollOffset: next };
41964
+ }
41965
+ function setTaskPanelScrollOffset(state, offset, maxOffset) {
41966
+ const next = Math.min(maxOffset, Math.max(0, offset));
41967
+ return next === state.taskPanelScrollOffset ? state : { ...state, taskPanelScrollOffset: next };
41921
41968
  }
41922
41969
  function activeTopic(state) {
41923
41970
  return state.topics.find((topic) => topic.id === state.activeTopicId) ?? null;
@@ -41934,6 +41981,16 @@ function setTopicUsage(state, usage) {
41934
41981
  function activeQuestion(state) {
41935
41982
  return activeMessages(state).slice().reverse().find((message2) => message2.kind === "ask_user_question" && message2.askUserQuestion && !message2.askUserQuestion.expired && !message2.askUserQuestion.selectedLabel) ?? null;
41936
41983
  }
41984
+ function blockedQuestionText(state, topicId) {
41985
+ const messages = state.messages[topicId] ?? [];
41986
+ for (let index = messages.length - 1;index >= 0; index -= 1) {
41987
+ const message2 = messages[index];
41988
+ if (message2.kind === "ask_user_question" && message2.askUserQuestion && !message2.askUserQuestion.expired && !message2.askUserQuestion.selectedLabel) {
41989
+ return message2.askUserQuestion.question;
41990
+ }
41991
+ }
41992
+ return;
41993
+ }
41937
41994
  function activeVaultRequest(state) {
41938
41995
  return activeMessages(state).find((message2) => message2.kind === "vault_secret_request" && message2.vaultSecretRequest?.status === "pending") ?? null;
41939
41996
  }
@@ -42037,12 +42094,43 @@ function visibleTopicPickerIds(state) {
42037
42094
  function visibleBackgroundSessions(state) {
42038
42095
  return state.backgroundSessions.filter((session) => topicPickerQueryMatches(session.title, state.topicFilter));
42039
42096
  }
42097
+ function topicRank(state, topic) {
42098
+ if (blockedQuestionText(state, topic.id) !== undefined)
42099
+ return 0;
42100
+ if (state.activity[topic.id]?.running)
42101
+ return 1;
42102
+ return 2;
42103
+ }
42104
+ function groupTopicsIntoUnits(entries) {
42105
+ const units = [];
42106
+ for (const entry of entries) {
42107
+ const last = units.at(-1);
42108
+ if (entry.topic.isSubagent && last)
42109
+ last.push(entry);
42110
+ else
42111
+ units.push([entry]);
42112
+ }
42113
+ return units;
42114
+ }
42115
+ function sortedTopicEntries(state, entries) {
42116
+ const units = groupTopicsIntoUnits(entries);
42117
+ if (state.topicsPanelSort === "name") {
42118
+ return units.slice().sort((a, b) => (a[0]?.topic.title ?? "").localeCompare(b[0]?.topic.title ?? "")).flat();
42119
+ }
42120
+ return units.map((unit, order) => ({
42121
+ unit,
42122
+ order,
42123
+ rank: Math.min(...unit.map(({ topic }) => topicRank(state, topic)))
42124
+ })).sort((a, b) => a.rank - b.rank || a.order - b.order).flatMap(({ unit }) => unit);
42125
+ }
42040
42126
  function topicPickerItems(state) {
42041
42127
  const visible = visibleTopicPickerIds(state);
42042
42128
  const indexedTopics = state.topics.map((topic, index) => ({ topic, index })).filter(({ topic }) => visible.has(topic.id));
42129
+ const managerEntries = sortedTopicEntries(state, indexedTopics.filter(({ topic }) => topic.kind === "manager"));
42130
+ const regularEntries = sortedTopicEntries(state, indexedTopics.filter(({ topic }) => topic.kind !== "manager"));
42043
42131
  return [
42044
- ...indexedTopics.filter(({ topic }) => topic.kind === "manager").map(({ topic, index }) => ({ kind: "topic", id: topic.id, index })),
42045
- ...indexedTopics.filter(({ topic }) => topic.kind !== "manager").map(({ topic, index }) => ({ kind: "topic", id: topic.id, index })),
42132
+ ...managerEntries.map(({ topic, index }) => ({ kind: "topic", id: topic.id, index })),
42133
+ ...regularEntries.map(({ topic, index }) => ({ kind: "topic", id: topic.id, index })),
42046
42134
  ...visibleBackgroundSessions(state).map((session) => ({
42047
42135
  kind: "background",
42048
42136
  id: session.id
@@ -42334,6 +42422,22 @@ function failedToolText(text3) {
42334
42422
  return [title, ...mapped].join(`
42335
42423
  `);
42336
42424
  }
42425
+ function appendToolOutput(text3, content) {
42426
+ if (!content.trim())
42427
+ return text3;
42428
+ const [title = "Tool"] = text3.split(`
42429
+ `);
42430
+ if (TOOL_OUTPUT_EXCLUDED_PREFIXES.some((prefix) => title.startsWith(prefix)))
42431
+ return text3;
42432
+ if (text3.includes(`
42433
+ `))
42434
+ return text3;
42435
+ const lines = content.replace(/\r\n?/g, `
42436
+ `).replace(/\n+$/, "").split(`
42437
+ `);
42438
+ return [title, ...lines].join(`
42439
+ `);
42440
+ }
42337
42441
  function toolMessageId(status, toolUseId) {
42338
42442
  return `terminal-tool:${String(status.queryId ?? "query")}:${toolUseId}`;
42339
42443
  }
@@ -42433,14 +42537,17 @@ function applyAiStatus(state, topicId, raw, createdAt) {
42433
42537
  return state;
42434
42538
  const id = String(status.toolUseId ?? "");
42435
42539
  const failed = status.isError === true;
42436
- const tools = current3.tools.map((tool) => tool.id === id ? { ...tool, output: String(status.content ?? ""), status: failed ? "error" : "done" } : tool);
42540
+ const content = String(status.content ?? "");
42541
+ const tools = current3.tools.map((tool) => tool.id === id ? { ...tool, output: content, status: failed ? "error" : "done" } : tool);
42437
42542
  const withActivity = setActivity(state, topicId, { ...liveCurrent, tools });
42438
42543
  const messageId = toolMessageId(status, id);
42439
42544
  const existing = (withActivity.messages[topicId] ?? []).find((message2) => message2.id === messageId);
42545
+ const withOutput = existing ? appendToolOutput(existing.text, content) : undefined;
42546
+ const nextText = failed && (!existing || withOutput === existing.text) ? failedToolText(existing?.text ?? "") : withOutput;
42440
42547
  return patchMessage(withActivity, topicId, messageId, {
42441
42548
  editedAt: createdAt ?? new Date().toISOString(),
42442
42549
  toolResult: failed ? "error" : "ok",
42443
- ...failed && existing ? { text: failedToolText(existing.text) } : {}
42550
+ ...nextText !== undefined ? { text: nextText } : {}
42444
42551
  });
42445
42552
  }
42446
42553
  if (kind === "tool_status") {
@@ -42473,11 +42580,18 @@ function applyRuntimeEvent(state, event) {
42473
42580
  }
42474
42581
  return state;
42475
42582
  }
42476
- var NO_TOPIC_PICKER_SELECTION = -1;
42583
+ var NO_TOPIC_PICKER_SELECTION = -1, TOOL_OUTPUT_EXCLUDED_PREFIXES;
42477
42584
  var init_state2 = __esm(async () => {
42478
42585
  init_decision_graph();
42479
42586
  init_subagent_graph();
42480
42587
  await init_src();
42588
+ TOOL_OUTPUT_EXCLUDED_PREFIXES = [
42589
+ "Edit \xB7 ",
42590
+ "Write \xB7 ",
42591
+ "Delete \xB7 ",
42592
+ "Ask session \xB7 ",
42593
+ "Tell session \xB7 "
42594
+ ];
42481
42595
  });
42482
42596
 
42483
42597
  // ../../adapters/terminal/src/context-usage.ts
@@ -43590,7 +43704,6 @@ function toolMessageLines(message2, width) {
43590
43704
  `);
43591
43705
  const failed = message2.toolResult === "error";
43592
43706
  const done = failed || Boolean(message2.editedAt);
43593
- const fileMutation = /^(?:Edit|Write|Delete) \u00B7 /.test(title);
43594
43707
  const wrapWidth = Math.max(4, width - 6);
43595
43708
  const marker = failed ? "!" : done ? "\u2713" : "\u25CF";
43596
43709
  const headerText = ` ${marker} ${title}`;
@@ -43606,14 +43719,8 @@ function toolMessageLines(message2, width) {
43606
43719
  { text: ")" }
43607
43720
  ] : [{ text: headerText, fg: headerColor }]
43608
43721
  });
43609
- if (!fileMutation) {
43610
- return [
43611
- header,
43612
- ...details.slice(0, 2).flatMap((detail) => wrapText(detail, wrapWidth).slice(0, 1).map((text3) => line(` ${text3}`, { fg: toolDetailColor(detail), dim: !failed })))
43613
- ];
43614
- }
43615
43722
  const wrapped = details.flatMap((detail) => wrapText(detail, wrapWidth).map((text3) => ({ detail, text: text3 })));
43616
- const visible = wrapped.slice(0, FILE_PREVIEW_MAX_LINES);
43723
+ const visible = wrapped.slice(0, TOOL_PREVIEW_MAX_LINES);
43617
43724
  const hidden = wrapped.length - visible.length;
43618
43725
  return [
43619
43726
  header,
@@ -43718,50 +43825,255 @@ function taskLines(state, width) {
43718
43825
  const visibleTasks = tasks3.slice(0, INLINE_TASK_LIMIT);
43719
43826
  const hidden = tasks3.length - visibleTasks.length;
43720
43827
  return [
43721
- line(" \u25EB Tasks \xB7 Ctrl-T sidebar", { fg: theme.amber, bold: true }),
43828
+ line(" \u25EB Tasks \xB7 Ctrl-P sidebar", { fg: theme.amber, bold: true }),
43722
43829
  ...visibleTasks.flatMap((task) => wrapText(task.trimStart(), Math.max(4, width - 6)).map((text3) => line(` ${text3}`, { fg: theme.muted }))),
43723
43830
  ...hidden > 0 ? [line(` +${hidden} more`, { fg: theme.muted, dim: true })] : [],
43724
43831
  line("")
43725
43832
  ];
43726
43833
  }
43727
43834
  function terminalBodyLayout(state, width) {
43728
- const showTaskSidebar = width >= TASK_SIDEBAR_MIN_WIDTH && state.taskSidebarEnabled && !state.overlay && !state.creatingTopic && taskItems(state).length > 0;
43835
+ const wideEnough = width >= SIDE_PANEL_MIN_WIDTH && state.topicsPanelEnabled && (!state.overlay || state.overlay === "background-session") && !state.creatingTopic;
43836
+ const showTaskPanel = wideEnough && taskItems(state).length > 0;
43837
+ const showTopicsPanel = wideEnough && (state.topics.length > 1 || visibleBackgroundSessions(state).length > 0);
43838
+ const showSide = showTaskPanel || showTopicsPanel;
43729
43839
  return {
43730
- conversationWidth: showTaskSidebar ? width - TASK_SIDEBAR_WIDTH - TASK_SIDEBAR_GAP : width,
43731
- taskWidth: showTaskSidebar ? TASK_SIDEBAR_WIDTH : 0,
43732
- showTaskSidebar
43733
- };
43840
+ conversationWidth: showSide ? width - SIDE_PANEL_WIDTH - SIDE_PANEL_GAP : width,
43841
+ sideWidth: showSide ? SIDE_PANEL_WIDTH : 0,
43842
+ showTaskPanel,
43843
+ showTopicsPanel
43844
+ };
43845
+ }
43846
+ function topicStatusIndicator(state, topic, animationFrame) {
43847
+ const blockedText = blockedQuestionText(state, topic.id);
43848
+ if (blockedText !== undefined)
43849
+ return { icon: "?", fg: theme.amber, blockedText };
43850
+ if (state.activity[topic.id]?.running) {
43851
+ return { icon: workingFrame(animationFrame), fg: theme.green };
43852
+ }
43853
+ return { icon: "\u25CB", fg: theme.muted };
43854
+ }
43855
+ function topicPanelRowLines(state, topic, topicIndex, animationFrame) {
43856
+ const selected = topic.id === state.activeTopicId;
43857
+ const hovered = !selected && topic.id === state.sidePanelHoverTopicId;
43858
+ const status = topicStatusIndicator(state, topic, animationFrame);
43859
+ const childPrefix = subagentTreePrefix(topic, state.topics);
43860
+ const bg2 = selected || hovered ? theme.selected : theme.canvas;
43861
+ const header = line(` ${selected ? "\u203A" : " "} ${childPrefix}${status.icon} ${topic.title}`, {
43862
+ fg: selected ? theme.text : status.fg,
43863
+ bg: bg2,
43864
+ bold: selected,
43865
+ pickerTarget: { topicIndex }
43866
+ });
43867
+ if (status.blockedText === undefined)
43868
+ return [header];
43869
+ return [
43870
+ header,
43871
+ line(` ${status.blockedText}`, {
43872
+ fg: theme.amber,
43873
+ bg: bg2,
43874
+ pickerTarget: { topicIndex }
43875
+ })
43876
+ ];
43877
+ }
43878
+ function topicPanelGroupLines(state, label, entries, animationFrame) {
43879
+ if (entries.length === 0)
43880
+ return [];
43881
+ return [
43882
+ line(` ${label}`, { fg: theme.cyan, dim: true, bold: true, bg: theme.canvas }),
43883
+ ...sortedTopicEntries(state, entries).flatMap(({ topic, topicIndex }) => topicPanelRowLines(state, topic, topicIndex, animationFrame))
43884
+ ];
43885
+ }
43886
+ function topicsPanelRows(state, animationFrame) {
43887
+ const indexed = state.topics.map((topic, topicIndex) => ({ topic, topicIndex }));
43888
+ const managerEntries = indexed.filter(({ topic }) => topic.kind === "manager");
43889
+ const regularEntries = indexed.filter(({ topic }) => topic.kind !== "manager");
43890
+ const topicRows = [
43891
+ ...topicPanelGroupLines(state, "manager", managerEntries, animationFrame),
43892
+ ...topicPanelGroupLines(state, "topics", regularEntries, animationFrame)
43893
+ ];
43894
+ const sessionRows = ["cron", "memory", "compact"].flatMap((kind) => {
43895
+ const matching = visibleBackgroundSessions(state).filter((session) => session.kind === kind);
43896
+ if (matching.length === 0)
43897
+ return [];
43898
+ return [
43899
+ line(` ${kind}`, { fg: theme.cyan, dim: true, bold: true, bg: theme.canvas }),
43900
+ ...matching.map((session) => {
43901
+ const hovered = session.id === state.sidePanelHoverBackgroundSessionId;
43902
+ return line(` ${session.active ? workingFrame(animationFrame) : "\u25CB"} ${session.title}`, {
43903
+ fg: hovered ? theme.text : session.active ? theme.green : theme.muted,
43904
+ bg: hovered ? theme.selected : theme.canvas,
43905
+ pickerTarget: { backgroundSessionId: session.id }
43906
+ });
43907
+ })
43908
+ ];
43909
+ });
43910
+ return [...topicRows, ...sessionRows];
43734
43911
  }
43735
- function taskSidebarLines(state, width, height) {
43912
+ function taskPanelRows(state, width) {
43736
43913
  const items = taskItems(state);
43737
- const groups = items.map((task) => {
43914
+ return items.flatMap((task) => {
43738
43915
  const completed = /^\s*(?:\[x\]|\u2612|\u2713|\u2705)/i.test(task);
43739
- return wrapText(task.trimStart(), Math.max(4, width - 4)).map((text3) => line(` ${text3}`, {
43916
+ return wrapText(task.trimStart(), Math.max(4, width - 2)).map((text3) => line(` ${text3}`, {
43740
43917
  fg: completed ? theme.muted : theme.text,
43741
- dim: completed
43918
+ dim: completed,
43919
+ bg: theme.canvas
43742
43920
  }));
43743
43921
  });
43744
- const capacity = Math.max(1, height);
43745
- const total = groups.reduce((sum, group) => sum + group.length, 0);
43746
- if (total <= capacity)
43747
- return groups.flat();
43748
- if (capacity === 1)
43749
- return groups[0]?.slice(0, 1) ?? [];
43750
- const rows = [];
43751
- let shown = 0;
43752
- for (const group of groups) {
43753
- if (rows.length + group.length > capacity - 1) {
43754
- if (shown === 0) {
43755
- rows.push(...group.slice(0, capacity - 1));
43756
- shown = 1;
43757
- }
43758
- break;
43922
+ }
43923
+ function panelHeaderHeight(sectionHeight) {
43924
+ return Math.min(2, sectionHeight);
43925
+ }
43926
+ function panelContentHeight(sectionHeight) {
43927
+ return Math.max(0, sectionHeight - panelHeaderHeight(sectionHeight));
43928
+ }
43929
+ function scrollbarThumb(totalRows, viewportRows, offset) {
43930
+ if (viewportRows <= 0 || totalRows <= 0)
43931
+ return { top: 0, len: 0 };
43932
+ const len = Math.max(1, Math.min(viewportRows, Math.round(viewportRows * viewportRows / totalRows)));
43933
+ const maxThumbTop = Math.max(0, viewportRows - len);
43934
+ const maxOffset = Math.max(1, totalRows - viewportRows);
43935
+ const top = maxThumbTop === 0 ? 0 : Math.round(Math.min(offset, maxOffset) * maxThumbTop / maxOffset);
43936
+ return { top, len };
43937
+ }
43938
+ function scrollOffsetFromDragRow(totalRows, viewportRows, trackRow, grabRowOffset) {
43939
+ if (viewportRows <= 0 || totalRows <= 0)
43940
+ return 0;
43941
+ const { len } = scrollbarThumb(totalRows, viewportRows, 0);
43942
+ const maxThumbTop = Math.max(0, viewportRows - len);
43943
+ if (maxThumbTop === 0)
43944
+ return 0;
43945
+ const clampedRow = Math.max(0, Math.min(trackRow, viewportRows - 1));
43946
+ const desiredTop = Math.min(maxThumbTop, Math.max(0, clampedRow - grabRowOffset));
43947
+ const maxOffset = Math.max(0, totalRows - viewportRows);
43948
+ return Math.round(desiredTop * maxOffset / maxThumbTop);
43949
+ }
43950
+ function scrollbarCell(totalRows, viewportRows, offset, rowIndex, accent) {
43951
+ const thumb = scrollbarThumb(totalRows, viewportRows, offset);
43952
+ const isThumb = rowIndex >= thumb.top && rowIndex < thumb.top + thumb.len;
43953
+ return paint(isThumb ? "\u2590" : "\u2595", { fg: isThumb ? accent : theme.muted, bg: theme.canvas });
43954
+ }
43955
+ function renderPanelSection(label, rows, width, height, scrollOffset, accent) {
43956
+ if (height <= 0 || width <= 0)
43957
+ return { lines: [], rowTargets: [] };
43958
+ const headerHeight = panelHeaderHeight(height);
43959
+ const contentHeight = panelContentHeight(height);
43960
+ const overflow = rows.length > contentHeight;
43961
+ const textWidth = overflow ? Math.max(1, width - 1) : width;
43962
+ const clampedOffset = Math.max(0, Math.min(scrollOffset, Math.max(0, rows.length - contentHeight)));
43963
+ const visible = rows.slice(clampedOffset, clampedOffset + contentHeight);
43964
+ const lines = [];
43965
+ if (headerHeight >= 1)
43966
+ lines.push(paint("\u2500".repeat(width), { fg: theme.border, bg: theme.canvas }));
43967
+ if (headerHeight >= 2) {
43968
+ lines.push(paint(fit(` ${label}`, width), { fg: accent, bg: theme.canvas, bold: true }));
43969
+ }
43970
+ const rowTargets = [];
43971
+ for (let index = 0;index < contentHeight; index += 1) {
43972
+ const item = visible[index];
43973
+ const text3 = paint(fit(item?.text ?? "", textWidth), {
43974
+ fg: item?.fg ?? theme.text,
43975
+ bg: item?.bg ?? theme.canvas,
43976
+ bold: item?.bold,
43977
+ dim: item?.dim
43978
+ });
43979
+ const scrollCell = overflow ? scrollbarCell(rows.length, contentHeight, clampedOffset, index, accent) : "";
43980
+ lines.push(`${text3}${scrollCell}`);
43981
+ if (item?.pickerTarget)
43982
+ rowTargets.push({ y: headerHeight + index, pickerTarget: item.pickerTarget });
43983
+ }
43984
+ return {
43985
+ lines,
43986
+ rowTargets,
43987
+ track: overflow ? { x: width - 1, viewportRows: contentHeight, rowCount: rows.length } : undefined
43988
+ };
43989
+ }
43990
+ function sidePanelSectionHeights(layout, bodyHeight) {
43991
+ if (layout.showTopicsPanel && layout.showTaskPanel) {
43992
+ const topicsHeight = Math.max(3, Math.floor(bodyHeight / 2));
43993
+ return { topicsHeight, taskHeight: Math.max(0, bodyHeight - topicsHeight) };
43994
+ }
43995
+ if (layout.showTopicsPanel)
43996
+ return { topicsHeight: bodyHeight, taskHeight: 0 };
43997
+ if (layout.showTaskPanel)
43998
+ return { topicsHeight: 0, taskHeight: bodyHeight };
43999
+ return { topicsHeight: 0, taskHeight: 0 };
44000
+ }
44001
+ function renderSideColumn(state, layout, bodyHeight, animationFrame) {
44002
+ const { showTaskPanel, showTopicsPanel, sideWidth } = layout;
44003
+ if (!showTaskPanel && !showTopicsPanel)
44004
+ return { lines: [], targets: [], tracks: [] };
44005
+ const { topicsHeight, taskHeight } = sidePanelSectionHeights(layout, bodyHeight);
44006
+ const sections = [];
44007
+ if (showTopicsPanel) {
44008
+ sections.push({
44009
+ panel: "topics",
44010
+ label: state.topicsPanelSort === "activity" ? "ACTIVITY \xB7 name" : "activity \xB7 NAME",
44011
+ rows: topicsPanelRows(state, animationFrame),
44012
+ accent: theme.accent,
44013
+ height: topicsHeight,
44014
+ offset: 0,
44015
+ scrollOffset: state.topicsPanelScrollOffset
44016
+ });
44017
+ }
44018
+ if (showTaskPanel) {
44019
+ sections.push({
44020
+ panel: "tasks",
44021
+ label: "tasks",
44022
+ rows: taskPanelRows(state, sideWidth),
44023
+ accent: theme.taskBorder,
44024
+ height: taskHeight,
44025
+ offset: showTopicsPanel ? topicsHeight : 0,
44026
+ scrollOffset: state.taskPanelScrollOffset
44027
+ });
44028
+ }
44029
+ const lines = [];
44030
+ const targets = [];
44031
+ const tracks = [];
44032
+ for (const section of sections) {
44033
+ const result = renderPanelSection(section.label, section.rows, sideWidth, section.height, section.scrollOffset, section.accent);
44034
+ lines.push(...result.lines);
44035
+ for (const target of result.rowTargets) {
44036
+ targets.push({ y: section.offset + target.y, pickerTarget: target.pickerTarget });
44037
+ }
44038
+ if (result.track) {
44039
+ tracks.push({
44040
+ panel: section.panel,
44041
+ x: result.track.x,
44042
+ yStart: section.offset + panelHeaderHeight(section.height),
44043
+ viewportRows: result.track.viewportRows,
44044
+ rowCount: result.track.rowCount
44045
+ });
43759
44046
  }
43760
- rows.push(...group);
43761
- shown += 1;
43762
44047
  }
43763
- rows.push(line(` +${items.length - shown} more`, { fg: theme.muted, dim: true }));
43764
- return rows;
44048
+ while (lines.length < bodyHeight)
44049
+ lines.push(paint(" ".repeat(sideWidth), { bg: theme.canvas }));
44050
+ return { lines: lines.slice(0, bodyHeight), targets, tracks };
44051
+ }
44052
+ function maxTopicsPanelScrollOffset(state, columns, rows) {
44053
+ const width = Math.max(1, columns);
44054
+ const height = Math.max(14, rows);
44055
+ if (state.overlay)
44056
+ return 0;
44057
+ const layout = terminalBodyLayout(state, width);
44058
+ if (!layout.showTopicsPanel)
44059
+ return 0;
44060
+ const bodyHeight = Math.max(3, height - footerLines(state, width).length - decisionPane(state, width).length - vaultRequestPane(state, width).length - composerPane(state, layout.conversationWidth).lines.length);
44061
+ const { topicsHeight } = sidePanelSectionHeights(layout, bodyHeight);
44062
+ const viewport = panelContentHeight(topicsHeight);
44063
+ return Math.max(0, topicsPanelRows(state, 0).length - viewport);
44064
+ }
44065
+ function maxTaskPanelScrollOffset(state, columns, rows) {
44066
+ const width = Math.max(1, columns);
44067
+ const height = Math.max(14, rows);
44068
+ if (state.overlay)
44069
+ return 0;
44070
+ const layout = terminalBodyLayout(state, width);
44071
+ if (!layout.showTaskPanel)
44072
+ return 0;
44073
+ const bodyHeight = Math.max(3, height - footerLines(state, width).length - decisionPane(state, width).length - vaultRequestPane(state, width).length - composerPane(state, layout.conversationWidth).lines.length);
44074
+ const { taskHeight } = sidePanelSectionHeights(layout, bodyHeight);
44075
+ const viewport = panelContentHeight(taskHeight);
44076
+ return Math.max(0, taskPanelRows(state, layout.sideWidth).length - viewport);
43765
44077
  }
43766
44078
  function helpLines() {
43767
44079
  return [
@@ -43771,7 +44083,9 @@ function helpLines() {
43771
44083
  line(" \u2190 \u2192 move \xB7 Ctrl/Alt-\u2190 \u2192 move by word \xB7 \u2191 \u2193 history"),
43772
44084
  line(" Alt-Backspace delete word \xB7 Ctrl-U clear before cursor"),
43773
44085
  line(" Cmd-Backspace works when forwarded \xB7 Ctrl-W/K aliases"),
43774
- line(" Mouse wheel / PgUp/PgDn scroll \xB7 Ctrl-E load older \xB7 Ctrl-T tasks"),
44086
+ line(" Mouse wheel / PgUp/PgDn scroll \xB7 Ctrl-E load older"),
44087
+ line(" Ctrl-P topics + tasks panel \xB7 click/hover a row to switch \xB7 drag scrollbar"),
44088
+ line(" Ctrl-R sort topics panel (activity / name)"),
43775
44089
  line(" Ctrl-O topics \xB7 Ctrl-G subagents \xB7 Ctrl-D decisions \xB7 Ctrl-X abort"),
43776
44090
  line(" In topics: type to filter \xB7 Ctrl-N new \xB7 Ctrl-D delete"),
43777
44091
  line(" Esc/Ctrl-C stop active turn \xB7 Ctrl-C twice when idle to quit"),
@@ -43923,8 +44237,8 @@ function topicOverlayCursor(state, width, bodyHeight) {
43923
44237
  function topicOverlayLines(state, width, height, animationFrame = 0) {
43924
44238
  const visibleIds = visibleTopicPickerIds(state);
43925
44239
  const indexedTopics = state.topics.map((topic, topicIndex) => ({ topic, topicIndex })).filter(({ topic }) => visibleIds.has(topic.id));
43926
- const managerTopics = indexedTopics.filter(({ topic }) => topic.kind === "manager");
43927
- const regularTopics = indexedTopics.filter(({ topic }) => topic.kind !== "manager");
44240
+ const managerTopics = sortedTopicEntries(state, indexedTopics.filter(({ topic }) => topic.kind === "manager"));
44241
+ const regularTopics = sortedTopicEntries(state, indexedTopics.filter(({ topic }) => topic.kind !== "manager"));
43928
44242
  const entries = [];
43929
44243
  for (const [label, topics] of [
43930
44244
  ["Manager", managerTopics],
@@ -43972,30 +44286,48 @@ function topicOverlayLines(state, width, height, animationFrame = 0) {
43972
44286
  query2.length > 0 ? line(`${TOPIC_FILTER_LABEL}${safeText(query2)}`, { fg: theme.cyan, bold: true }) : line(""),
43973
44287
  ...entries.length === 0 ? [
43974
44288
  line(query2.length > 0 ? ` No topics match \u201C${safeText(query2)}\u201D \xB7 Esc clears the filter` : " No topics yet \xB7 Press Ctrl-N to create one", { fg: theme.muted })
43975
- ] : entries.slice(start, start + visibleCount).map((entry) => {
44289
+ ] : entries.slice(start, start + visibleCount).flatMap((entry) => {
43976
44290
  if (entry.kind === "heading") {
43977
- return line(` ${entry.label}`, { fg: theme.cyan, bold: true });
44291
+ return [line(` ${entry.label}`, { fg: theme.cyan, bold: true })];
43978
44292
  }
43979
44293
  if (entry.kind === "separator") {
43980
- return line(` ${"\u2500".repeat(Math.max(1, width - 4))}`, { fg: theme.border });
44294
+ return [line(` ${"\u2500".repeat(Math.max(1, width - 4))}`, { fg: theme.border })];
43981
44295
  }
43982
44296
  if (entry.kind === "background") {
43983
44297
  const selected2 = entry.sessionId === state.topicPickerBackgroundId;
43984
- return line(` ${selected2 ? "\u203A" : " "} ${entry.active ? workingFrame(animationFrame) : "\u25CB"} ${entry.label} \xB7 ${entry.status}`, {
43985
- fg: selected2 ? theme.text : entry.active ? theme.green : theme.muted,
43986
- bg: selected2 ? theme.selected : theme.canvas,
43987
- bold: selected2
43988
- });
44298
+ return [
44299
+ line(` ${selected2 ? "\u203A" : " "} ${entry.active ? workingFrame(animationFrame) : "\u25CB"} ${entry.label} \xB7 ${entry.status}`, {
44300
+ fg: selected2 ? theme.text : entry.active ? theme.green : theme.muted,
44301
+ bg: selected2 ? theme.selected : theme.canvas,
44302
+ bold: selected2,
44303
+ pickerTarget: { backgroundSessionId: entry.sessionId }
44304
+ })
44305
+ ];
43989
44306
  }
43990
44307
  const { topic, topicIndex } = entry;
43991
44308
  const selected = topicIndex === state.topicPickerIndex;
43992
- const running = state.activity[topic.id]?.running;
44309
+ const status = topicStatusIndicator(state, topic, animationFrame);
43993
44310
  const childPrefix = subagentTreePrefix(topic, state.topics);
43994
- return line(` ${selected ? "\u203A" : " "} ${childPrefix}${running ? workingFrame(animationFrame) : "\u25CB"} ${topic.title} \xB7 ${topic.agent ?? "no agent"} \xB7 ${effectiveTopicModel(topic)} \xB7 ${effectiveTopicEffort(topic)}`, {
43995
- fg: selected ? theme.text : running ? theme.green : theme.muted,
43996
- bg: selected ? theme.selected : theme.canvas,
43997
- bold: selected
44311
+ const bg2 = selected ? theme.selected : theme.canvas;
44312
+ const header = line(` ${selected ? "\u203A" : " "} ${childPrefix}${status.icon} ${topic.title} \xB7 ${topic.agent ?? "no agent"} \xB7 ${effectiveTopicModel(topic)} \xB7 ${effectiveTopicEffort(topic)}`, {
44313
+ fg: selected ? theme.text : status.fg,
44314
+ bg: bg2,
44315
+ bold: selected,
44316
+ pickerTarget: { topicIndex }
43998
44317
  });
44318
+ const running = state.activity[topic.id]?.running;
44319
+ const subtitle = status.blockedText ?? (running ? activityDetail(state.activity[topic.id]?.status) : undefined);
44320
+ if (!subtitle)
44321
+ return [header];
44322
+ return [
44323
+ header,
44324
+ line(` ${subtitle}`, {
44325
+ fg: status.blockedText !== undefined ? theme.amber : theme.muted,
44326
+ dim: status.blockedText === undefined,
44327
+ bg: bg2,
44328
+ pickerTarget: { topicIndex }
44329
+ })
44330
+ ];
43999
44331
  })
44000
44332
  ];
44001
44333
  }
@@ -44341,6 +44673,16 @@ function conversationViewport(lineCount, height, requestedOffset) {
44341
44673
  offset: Math.min(maxOffset, Math.max(0, requestedOffset))
44342
44674
  };
44343
44675
  }
44676
+ function popupPaneWidth(width) {
44677
+ return Math.max(0, Math.min(POPUP_PANE_MAX_WIDTH, width - 4));
44678
+ }
44679
+ function centerPaneRows(rows, boxWidth, totalWidth) {
44680
+ const left = Math.max(0, Math.floor((totalWidth - boxWidth) / 2));
44681
+ const right = Math.max(0, totalWidth - boxWidth - left);
44682
+ const leftPad = left > 0 ? paint(" ".repeat(left), { bg: theme.canvas }) : "";
44683
+ const rightPad = right > 0 ? paint(" ".repeat(right), { bg: theme.canvas }) : "";
44684
+ return rows.map((row) => `${leftPad}${row}${rightPad}`);
44685
+ }
44344
44686
  function decisionPane(state, width) {
44345
44687
  if (state.overlay)
44346
44688
  return [];
@@ -44351,6 +44693,7 @@ function decisionPane(state, width) {
44351
44693
  if (!ask || !question?.choices.length)
44352
44694
  return [];
44353
44695
  const selected = Math.min(state.askChoiceIndex, question.choices.length - 1);
44696
+ const boxWidth = popupPaneWidth(width);
44354
44697
  const content = [
44355
44698
  line(` ${question.question}`, { bold: true }),
44356
44699
  ...question.choices.map((choice, index) => line(` ${index === selected ? "\u25CF" : "\u25CB"} ${choice.label}${choice.description ? ` \u2014 ${choice.description}` : ""}`, {
@@ -44359,7 +44702,8 @@ function decisionPane(state, width) {
44359
44702
  bold: index === selected
44360
44703
  }))
44361
44704
  ];
44362
- return framePane("decision required \xB7 \u2191\u2193 select \xB7 Enter confirm", content, width, Math.min(8, Math.max(4, content.length + 2)), { active: true, accent: theme.amber });
44705
+ const rows = framePane("decision required \xB7 \u2191\u2193 select \xB7 Enter confirm", content, boxWidth, Math.min(8, Math.max(4, content.length + 2)), { active: true, accent: theme.amber });
44706
+ return centerPaneRows(rows, boxWidth, width);
44363
44707
  }
44364
44708
  function vaultRequestPane(state, width) {
44365
44709
  if (state.overlay)
@@ -44368,15 +44712,17 @@ function vaultRequestPane(state, width) {
44368
44712
  if (!request)
44369
44713
  return [];
44370
44714
  const declining = state.vaultRequestMode === "confirm-decline";
44715
+ const boxWidth = popupPaneWidth(width);
44371
44716
  const content = [
44372
44717
  line(` ${request.key}`, { bold: true }),
44373
- ...request.description ? wrapText(request.description, Math.max(4, width - 6)).map((text3) => line(` ${text3}`, { fg: theme.muted })) : [],
44718
+ ...request.description ? wrapText(request.description, Math.max(4, boxWidth - 6)).map((text3) => line(` ${text3}`, { fg: theme.muted })) : [],
44374
44719
  declining ? line(" Decline this request? y / n", { fg: theme.red, bold: true }) : line(" Enter save to Vault \xB7 Esc clear / decline \xB7 Alt-Enter newline", {
44375
44720
  fg: theme.subtle
44376
44721
  }),
44377
44722
  ...state.vaultRequestNotice ? [line(` ! ${state.vaultRequestNotice}`, { fg: theme.amber })] : []
44378
44723
  ];
44379
- return framePane(`secret requested by ${state.aiName} \xB7 never shown to the agent${state.vaultRequestSubmitting ? ` \xB7 ${workingFrame(0)} saving` : ""}`, content, width, Math.min(8, Math.max(4, content.length + 2)), { active: true, accent: theme.accent });
44724
+ const rows = framePane(`secret requested by ${state.aiName} \xB7 never shown to the agent${state.vaultRequestSubmitting ? ` \xB7 ${workingFrame(0)} saving` : ""}`, content, boxWidth, Math.min(8, Math.max(4, content.length + 2)), { active: true, accent: theme.accent });
44725
+ return centerPaneRows(rows, boxWidth, width);
44380
44726
  }
44381
44727
  function wrapLineWithCursor(value, codePointColumn, width) {
44382
44728
  const chars = Array.from(safeText(value));
@@ -44450,6 +44796,20 @@ function inputVisualLines(state, width) {
44450
44796
  }
44451
44797
  return { lines: result, cursorLine, cursorColumn };
44452
44798
  }
44799
+ function shiftLines(lines, marginWidth) {
44800
+ if (marginWidth <= 0)
44801
+ return lines;
44802
+ const margin = paint(" ".repeat(marginWidth), { bg: theme.canvas });
44803
+ return lines.map((row) => `${margin}${row}`);
44804
+ }
44805
+ function shiftComposerPane(pane, marginWidth) {
44806
+ if (marginWidth <= 0)
44807
+ return pane;
44808
+ return {
44809
+ lines: shiftLines(pane.lines, marginWidth),
44810
+ cursor: { x: pane.cursor.x + marginWidth, y: pane.cursor.y }
44811
+ };
44812
+ }
44453
44813
  function composerPane(state, width) {
44454
44814
  const title = state.creatingTopic ? "new topic \xB7 type a name \xB7 Enter create" : state.overlay === "vault" ? state.vaultMode === "list" ? "Vault command \xB7 Enter run \xB7 Esc close" : `${state.vaultMode === "key" ? "key" : state.vaultMode === "value" ? "secret value" : "description"} \xB7 Enter continue \xB7 Esc cancel` : "Ctrl-O topics \xB7 Ctrl-G subagents \xB7 Ctrl-D decisions";
44455
44815
  const visual = inputVisualLines(state, width);
@@ -44693,30 +45053,62 @@ function renderApp(state, columns, rows, animationFrame = 0, nowMs = terminalNow
44693
45053
  function renderAppFrame(state, columns, rows, animationFrame = 0, nowMs = terminalNowMs()) {
44694
45054
  const width = Math.max(1, columns);
44695
45055
  const height = Math.max(14, rows);
44696
- const footer = footerLines(state, width);
44697
- const decision = [...decisionPane(state, width), ...vaultRequestPane(state, width)];
45056
+ const layout = terminalBodyLayout(state, width);
45057
+ const conversationXStart = layout.sideWidth > 0 ? layout.sideWidth + SIDE_PANEL_GAP + 1 : 1;
45058
+ const footer = shiftLines(footerLines(state, layout.conversationWidth), conversationXStart - 1);
45059
+ const decision = shiftLines([
45060
+ ...decisionPane(state, layout.conversationWidth),
45061
+ ...vaultRequestPane(state, layout.conversationWidth)
45062
+ ], conversationXStart - 1);
44698
45063
  const hideComposer = state.overlay === "background-session" || state.overlay === "topics" || state.overlay === "subagents" || state.overlay === "decisions" || state.overlay === "vault" && state.vaultMode === "confirm-delete";
44699
- const composer = hideComposer ? { lines: [], cursor: null } : composerPane(state, width);
45064
+ const composer = hideComposer ? { lines: [], cursor: null } : shiftComposerPane(composerPane(state, layout.conversationWidth), conversationXStart - 1);
44700
45065
  const bodyHeight = Math.max(3, height - footer.length - decision.length - composer.lines.length);
44701
- const layout = terminalBodyLayout(state, width);
44702
- const conversation = conversationLines(state, layout.conversationWidth, bodyHeight, animationFrame, nowMs, !layout.showTaskSidebar);
45066
+ const conversation = conversationLines(state, layout.conversationWidth, bodyHeight, animationFrame, nowMs, !layout.showTaskPanel);
44703
45067
  const conversationBody = renderBody(conversation, layout.conversationWidth, bodyHeight);
44704
- const body = layout.showTaskSidebar ? (() => {
44705
- const taskPane = framePane("Tasks \xB7 Ctrl-T", taskSidebarLines(state, layout.taskWidth, Math.max(0, bodyHeight - 2)), layout.taskWidth, bodyHeight, { active: true, accent: theme.taskBorder });
44706
- if (taskPane.length === 0)
44707
- return renderBody(conversation, width, bodyHeight);
44708
- const gap = paint(" ".repeat(TASK_SIDEBAR_GAP), { bg: theme.canvas });
44709
- return conversationBody.map((row, index) => `${row}${gap}${taskPane[index] ?? ""}`);
45068
+ const sideColumn = layout.showTaskPanel || layout.showTopicsPanel ? renderSideColumn(state, layout, bodyHeight, animationFrame) : null;
45069
+ const body = sideColumn?.lines.length ? (() => {
45070
+ const gap = paint(" ".repeat(SIDE_PANEL_GAP), { bg: theme.canvas });
45071
+ return sideColumn.lines.map((row, index) => `${row}${gap}${conversationBody[index] ?? ""}`);
44710
45072
  })() : conversationBody;
44711
45073
  const codeCopyTargets = conversation.flatMap((item, index) => {
44712
45074
  if (item.codeCopy === undefined || index >= bodyHeight)
44713
45075
  return [];
44714
45076
  const trimmed = item.text.trimEnd();
44715
45077
  const leading = trimmed.length - trimmed.trimStart().length;
44716
- const xStart = displayWidth(trimmed.slice(0, leading)) + 1;
44717
- const xEnd = Math.min(displayWidth(trimmed), layout.conversationWidth);
45078
+ const xStart = conversationXStart + displayWidth(trimmed.slice(0, leading));
45079
+ const xEnd = Math.min(conversationXStart - 1 + displayWidth(trimmed), width);
44718
45080
  return xEnd >= xStart ? [{ xStart, xEnd, y: index + 1, text: item.codeCopy }] : [];
44719
45081
  });
45082
+ const overlayTargets = conversation.flatMap((item, index) => {
45083
+ if (!item.pickerTarget || index >= bodyHeight)
45084
+ return [];
45085
+ return [{ xStart: 1, xEnd: layout.conversationWidth, y: index + 1, ...item.pickerTarget }];
45086
+ });
45087
+ const sidePanelTargets = (sideColumn?.targets ?? []).map((target) => ({
45088
+ xStart: 1,
45089
+ xEnd: layout.sideWidth,
45090
+ y: target.y + 1,
45091
+ ...target.pickerTarget
45092
+ }));
45093
+ const topicPickerTargets = [...overlayTargets, ...sidePanelTargets];
45094
+ const sidePanelTracks = (sideColumn?.tracks ?? []).map((track) => ({
45095
+ ...track,
45096
+ x: 1 + track.x,
45097
+ yStart: track.yStart + 1
45098
+ }));
45099
+ const sideColumnBounds = sideColumn ? (() => {
45100
+ const { topicsHeight, taskHeight } = sidePanelSectionHeights(layout, bodyHeight);
45101
+ const sections = [];
45102
+ let offset = 0;
45103
+ if (layout.showTopicsPanel) {
45104
+ sections.push({ panel: "topics", yStart: offset + 1, yEnd: offset + topicsHeight });
45105
+ offset += topicsHeight;
45106
+ }
45107
+ if (layout.showTaskPanel) {
45108
+ sections.push({ panel: "tasks", yStart: offset + 1, yEnd: offset + taskHeight });
45109
+ }
45110
+ return { xStart: 1, xEnd: layout.sideWidth, sections };
45111
+ })() : null;
44720
45112
  const cursorY = composer.cursor ? body.length + decision.length + composer.cursor.y : Number.POSITIVE_INFINITY;
44721
45113
  const overlayCursor = topicOverlayCursor(state, layout.conversationWidth, body.length);
44722
45114
  const cursor = composer.cursor && cursorY <= height ? { x: Math.min(composer.cursor.x, width), y: cursorY } : overlayCursor && overlayCursor.y <= height ? overlayCursor : null;
@@ -44724,7 +45116,10 @@ function renderAppFrame(state, columns, rows, animationFrame = 0, nowMs = termin
44724
45116
  frame: linkifyUrls([...body, ...decision, ...composer.lines, ...footer].slice(0, height).join(`
44725
45117
  `)),
44726
45118
  cursor,
44727
- codeCopyTargets
45119
+ codeCopyTargets,
45120
+ topicPickerTargets,
45121
+ sidePanelTracks,
45122
+ sideColumnBounds
44728
45123
  };
44729
45124
  }
44730
45125
  function maxConversationScrollOffset(state, columns, rows) {
@@ -44736,9 +45131,9 @@ function maxConversationScrollOffset(state, columns, rows) {
44736
45131
  }
44737
45132
  if (state.overlay)
44738
45133
  return 0;
44739
- const bodyHeight = Math.max(3, height - footerLines(state, width).length - decisionPane(state, width).length - vaultRequestPane(state, width).length - composerPane(state, width).lines.length);
44740
45134
  const layout = terminalBodyLayout(state, width);
44741
- const lineCount = conversationContentLines(state, layout.conversationWidth, 0, terminalNowMs(), !layout.showTaskSidebar).length;
45135
+ const bodyHeight = Math.max(3, height - footerLines(state, width).length - decisionPane(state, width).length - vaultRequestPane(state, width).length - composerPane(state, layout.conversationWidth).lines.length);
45136
+ const lineCount = conversationContentLines(state, layout.conversationWidth, 0, terminalNowMs(), !layout.showTaskPanel).length;
44742
45137
  return conversationViewport(lineCount, bodyHeight, state.scrollOffset).maxOffset;
44743
45138
  }
44744
45139
  function preserveConversationScrollAnchor(previous, next, columns, rows) {
@@ -44776,7 +45171,7 @@ var theme, WORKING_FRAMES, CODE_COPY_MARKER = "\u29C9", WORKING_FRAME_INTERVAL_M
44776
45171
  default:
44777
45172
  return `${ESC}48;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
44778
45173
  }
44779
- }, BARE_URL_PATTERN, URL_TRAILING_PUNCTUATION, RENDERED_TAB_WIDTH = 4, CIRCLED_DIGIT_START = 9312, CIRCLED_DIGIT_END = 9331, CIRCLED_ZERO = 9450, isWrapSpace = (character) => character === " " || character === "\t", isWideBreak = (character) => runeWidth(character) === 2, NO_ROW_START, NO_ROW_END, WRAP_MIN_WIDTH = 2, FRAME_MIN_INNER_WIDTH = 4, FRAME_TITLE_MIN_INNER_WIDTH = 8, TABLE_MAX_WIDTH_FRACTION = 0.92, TABLE_MIN_COL_WIDTH = 4, FILE_PREVIEW_MAX_LINES = 12, messageLayoutCache, INLINE_TASK_LIMIT = 5, TASK_SIDEBAR_MIN_WIDTH = 110, TASK_SIDEBAR_WIDTH = 36, TASK_SIDEBAR_GAP = 1, TOPIC_FILTER_ROW_INDEX = 2, TOPIC_FILTER_LABEL = " Filter: ", COMPOSER_GUTTER = 4, COMPOSER_RIGHT_MARGIN = 1, COMPOSER_MIN_CONTENT_WIDTH = 4, NOTICE_STYLE;
45174
+ }, BARE_URL_PATTERN, URL_TRAILING_PUNCTUATION, RENDERED_TAB_WIDTH = 4, CIRCLED_DIGIT_START = 9312, CIRCLED_DIGIT_END = 9331, CIRCLED_ZERO = 9450, isWrapSpace = (character) => character === " " || character === "\t", isWideBreak = (character) => runeWidth(character) === 2, NO_ROW_START, NO_ROW_END, WRAP_MIN_WIDTH = 2, FRAME_MIN_INNER_WIDTH = 4, FRAME_TITLE_MIN_INNER_WIDTH = 8, TABLE_MAX_WIDTH_FRACTION = 0.92, TABLE_MIN_COL_WIDTH = 4, TOOL_PREVIEW_MAX_LINES = 12, messageLayoutCache, INLINE_TASK_LIMIT = 5, SIDE_PANEL_MIN_WIDTH = 110, SIDE_PANEL_WIDTH = 36, SIDE_PANEL_GAP = 1, TOPIC_FILTER_ROW_INDEX = 2, TOPIC_FILTER_LABEL = " Filter: ", POPUP_PANE_MAX_WIDTH = 72, COMPOSER_GUTTER = 4, COMPOSER_RIGHT_MARGIN = 1, COMPOSER_MIN_CONTENT_WIDTH = 4, NOTICE_STYLE;
44780
45175
  var init_render = __esm(async () => {
44781
45176
  init_color_depth();
44782
45177
  init_commands();
@@ -45034,6 +45429,7 @@ function consumeMouseInput(raw) {
45034
45429
  let scrollDelta = 0;
45035
45430
  let horizontalScrollDelta = 0;
45036
45431
  const events = [];
45432
+ const wheelEvents = [];
45037
45433
  const input = raw.replace(SGR_MOUSE_PATTERN, (_sequence, rawButton, rawX, rawY, suffix) => {
45038
45434
  const button = Number.parseInt(rawButton, 10);
45039
45435
  if (Number.isFinite(button) && (button & 64) !== 0) {
@@ -45043,6 +45439,10 @@ function consumeMouseInput(raw) {
45043
45439
  horizontalScrollDelta += delta;
45044
45440
  else
45045
45441
  scrollDelta += delta;
45442
+ const x = Number.parseInt(rawX, 10);
45443
+ const y = Number.parseInt(rawY, 10);
45444
+ if (Number.isFinite(x) && Number.isFinite(y))
45445
+ wheelEvents.push({ x, y, delta, horizontal });
45046
45446
  } else {
45047
45447
  const x = Number.parseInt(rawX, 10);
45048
45448
  const y = Number.parseInt(rawY, 10);
@@ -45057,11 +45457,14 @@ function consumeMouseInput(raw) {
45057
45457
  }
45058
45458
  return "";
45059
45459
  });
45060
- return { input, scrollDelta, horizontalScrollDelta, events };
45460
+ return { input, scrollDelta, horizontalScrollDelta, events, wheelEvents };
45061
45461
  }
45062
45462
  function codeCopyTargetAt(targets, point) {
45063
45463
  return targets.find((target) => target.y === point.y && point.x >= target.xStart && point.x <= target.xEnd);
45064
45464
  }
45465
+ function topicPickerTargetAt(targets, point) {
45466
+ return targets.find((target) => target.y === point.y && point.x >= target.xStart && point.x <= target.xEnd);
45467
+ }
45065
45468
  function escapeStopsActiveTurn(state) {
45066
45469
  if (state.overlay || state.creatingTopic)
45067
45470
  return false;
@@ -46791,9 +47194,9 @@ function altScreenSequences(env = process.env, savedDepth) {
46791
47194
  const setBackground = colored ? "\x1B]11;#0a0b0f\x07" : "";
46792
47195
  const fillCanvas = canvasFill(depth2);
46793
47196
  const resetBackground = colored ? "\x1B]111\x07" : "";
46794
- const modeResets = "\x1B[0m\x1B[?7h\x1B[?1l\x1B[?1006l\x1B[?1002l\x1B[?2004l\x1B[?25h\x1B[?1049l";
47197
+ const modeResets = "\x1B[0m\x1B[?7h\x1B[?1l\x1B[?1006l\x1B[?1003l\x1B[?2004l\x1B[?25h\x1B[?1049l";
46795
47198
  return {
46796
- enter: `${setBackground}\x1B[?1049h${fillCanvas}\x1B[2J\x1B[H\x1B[?25l\x1B[?2004h\x1B[?1002h\x1B[?1006h\x1B[?1l${push}`,
47199
+ enter: `${setBackground}\x1B[?1049h${fillCanvas}\x1B[2J\x1B[H\x1B[?25l\x1B[?2004h\x1B[?1003h\x1B[?1006h\x1B[?1l${push}`,
46797
47200
  exit: `${END_SYNCHRONIZED_UPDATE}${pop}${modeResets}${resetBackground}`,
46798
47201
  abortEnter: `${END_SYNCHRONIZED_UPDATE}${modeResets}`
46799
47202
  };
@@ -46836,6 +47239,10 @@ class TerminalApp {
46836
47239
  #subagentGraphDragPoint = null;
46837
47240
  #plainFrameLines = [];
46838
47241
  #codeCopyTargets = [];
47242
+ #topicPickerTargets = [];
47243
+ #sidePanelTracks = [];
47244
+ #sideColumnBounds = null;
47245
+ #sidePanelDrag = null;
46839
47246
  #lastInterruptAt = 0;
46840
47247
  #subagentGraphGeneration = 0;
46841
47248
  #subagentGraphAbortController = null;
@@ -47211,6 +47618,9 @@ class TerminalApp {
47211
47618
  const rows = process.stdout.rows ?? 30;
47212
47619
  const rendered = renderAppFrame(this.#state, columns, rows, this.#animationFrame);
47213
47620
  this.#codeCopyTargets = rendered.codeCopyTargets;
47621
+ this.#topicPickerTargets = rendered.topicPickerTargets;
47622
+ this.#sidePanelTracks = rendered.sidePanelTracks;
47623
+ this.#sideColumnBounds = rendered.sideColumnBounds;
47214
47624
  const baseFrame = rendered.frame;
47215
47625
  this.#plainFrameLines = stripAnsi(baseFrame).split(`
47216
47626
  `);
@@ -47598,19 +48008,25 @@ class TerminalApp {
47598
48008
  }
47599
48009
  #handleKeySegment(segment) {
47600
48010
  const mouse = consumeMouseInput(segment);
47601
- if (mouse.horizontalScrollDelta !== 0 && (this.#state.overlay === "subagents" || this.#state.overlay === "decisions")) {
48011
+ const { scrollDelta, horizontalScrollDelta } = this.#routeWheelEvents(mouse.wheelEvents);
48012
+ if (horizontalScrollDelta !== 0 && (this.#state.overlay === "subagents" || this.#state.overlay === "decisions")) {
47602
48013
  this.#selection = null;
47603
- this.#panActiveGraph(-mouse.horizontalScrollDelta, 0);
48014
+ this.#panActiveGraph(-horizontalScrollDelta, 0);
47604
48015
  }
47605
- if (mouse.scrollDelta !== 0) {
48016
+ if (scrollDelta !== 0) {
47606
48017
  this.#selection = null;
47607
- this.#scroll(mouse.scrollDelta);
48018
+ this.#scroll(scrollDelta);
47608
48019
  }
47609
48020
  for (const event of mouse.events) {
47610
48021
  if (this.#state.overlay === "subagents" || this.#state.overlay === "decisions") {
47611
48022
  this.#handleGraphMouse(event);
47612
- } else
47613
- this.#handleMouseSelection(event);
48023
+ continue;
48024
+ }
48025
+ if (this.#handleSidePanelScrollbarDrag(event))
48026
+ continue;
48027
+ if (this.#handlePickableRowMouse(event))
48028
+ continue;
48029
+ this.#handleMouseSelection(event);
47614
48030
  }
47615
48031
  const chunk = normalizeKeySequences(mouse.input);
47616
48032
  if (!chunk)
@@ -47649,10 +48065,17 @@ class TerminalApp {
47649
48065
  this.#toggleDecisionGraph();
47650
48066
  return;
47651
48067
  }
47652
- if (chunk === "\x14") {
48068
+ if (chunk === "\x10") {
47653
48069
  if (editingVaultSecret)
47654
48070
  return;
47655
- this.#state = toggleTaskSidebar(this.#state);
48071
+ this.#state = toggleTopicsPanel(this.#state);
48072
+ this.#queueRender();
48073
+ return;
48074
+ }
48075
+ if (chunk === "\x12") {
48076
+ if (editingVaultSecret)
48077
+ return;
48078
+ this.#state = cycleTopicsPanelSort(this.#state);
47656
48079
  this.#queueRender();
47657
48080
  return;
47658
48081
  }
@@ -48790,6 +49213,140 @@ class TerminalApp {
48790
49213
  }
48791
49214
  this.#queueRender();
48792
49215
  }
49216
+ #routeWheelEvents(events) {
49217
+ let scrollDelta = 0;
49218
+ let horizontalScrollDelta = 0;
49219
+ if (events.length === 0)
49220
+ return { scrollDelta, horizontalScrollDelta };
49221
+ const bounds = this.#sideColumnBounds;
49222
+ let touchedPanel = false;
49223
+ for (const event of events) {
49224
+ const section = bounds && event.x >= bounds.xStart && event.x <= bounds.xEnd ? bounds.sections.find((s) => event.y >= s.yStart && event.y <= s.yEnd) : undefined;
49225
+ if (!section) {
49226
+ if (event.horizontal)
49227
+ horizontalScrollDelta += event.delta;
49228
+ else
49229
+ scrollDelta += event.delta;
49230
+ continue;
49231
+ }
49232
+ touchedPanel = true;
49233
+ const columns = process.stdout.columns ?? 100;
49234
+ const rows = process.stdout.rows ?? 30;
49235
+ this.#state = section.panel === "topics" ? scrollTopicsPanel(this.#state, -event.delta, maxTopicsPanelScrollOffset(this.#state, columns, rows)) : scrollTaskPanel(this.#state, -event.delta, maxTaskPanelScrollOffset(this.#state, columns, rows));
49236
+ }
49237
+ if (touchedPanel)
49238
+ this.#queueRender();
49239
+ return { scrollDelta, horizontalScrollDelta };
49240
+ }
49241
+ #handleSidePanelScrollbarDrag(event) {
49242
+ if (event.kind === "press" && (event.button & 3) === 0) {
49243
+ const track2 = this.#sidePanelTracks.find((t) => event.x === t.x && event.y >= t.yStart && event.y < t.yStart + t.viewportRows);
49244
+ if (!track2)
49245
+ return false;
49246
+ const current3 = track2.panel === "topics" ? this.#state.topicsPanelScrollOffset : this.#state.taskPanelScrollOffset;
49247
+ const thumb = scrollbarThumb(track2.rowCount, track2.viewportRows, current3);
49248
+ this.#sidePanelDrag = {
49249
+ panel: track2.panel,
49250
+ track: track2,
49251
+ grabRowOffset: event.y - (track2.yStart + thumb.top)
49252
+ };
49253
+ return true;
49254
+ }
49255
+ const drag = this.#sidePanelDrag;
49256
+ if (!drag)
49257
+ return false;
49258
+ if (event.kind === "release") {
49259
+ this.#sidePanelDrag = null;
49260
+ return true;
49261
+ }
49262
+ const { track, grabRowOffset, panel } = drag;
49263
+ const offset = scrollOffsetFromDragRow(track.rowCount, track.viewportRows, event.y - track.yStart, grabRowOffset);
49264
+ const columns = process.stdout.columns ?? 100;
49265
+ const rows = process.stdout.rows ?? 30;
49266
+ this.#state = panel === "topics" ? setTopicsPanelScrollOffset(this.#state, offset, maxTopicsPanelScrollOffset(this.#state, columns, rows)) : setTaskPanelScrollOffset(this.#state, offset, maxTaskPanelScrollOffset(this.#state, columns, rows));
49267
+ this.#queueRender();
49268
+ return true;
49269
+ }
49270
+ #handlePickableRowMouse(event) {
49271
+ const inOverlay = this.#state.overlay === "topics";
49272
+ const hovering = event.kind === "drag" && (event.button & 3) === 3;
49273
+ const isClick = event.kind === "press" && (event.button & 3) === 0;
49274
+ if (!inOverlay && !hovering && !isClick)
49275
+ return false;
49276
+ const target = topicPickerTargetAt(this.#topicPickerTargets, { x: event.x, y: event.y });
49277
+ if (inOverlay) {
49278
+ if (!target)
49279
+ return true;
49280
+ const highlighted = this.#highlightPickedTarget(target);
49281
+ if (!hovering && highlighted)
49282
+ this.#selectPickedTopic();
49283
+ return true;
49284
+ }
49285
+ if (target?.backgroundSessionId !== undefined) {
49286
+ const sessionId = target.backgroundSessionId;
49287
+ if (hovering) {
49288
+ this.#setSidePanelHover(undefined, sessionId);
49289
+ return true;
49290
+ }
49291
+ this.#setSidePanelHover(undefined, undefined);
49292
+ this.#state = {
49293
+ ...this.#state,
49294
+ overlay: "background-session",
49295
+ topicPickerBackgroundId: sessionId,
49296
+ backgroundScrollOffset: 0,
49297
+ notice: undefined,
49298
+ noticeLevel: undefined
49299
+ };
49300
+ this.#queueRender();
49301
+ return true;
49302
+ }
49303
+ if (target?.topicIndex === undefined) {
49304
+ this.#setSidePanelHover(undefined, undefined);
49305
+ return false;
49306
+ }
49307
+ const topic = this.#state.topics[target.topicIndex];
49308
+ if (!topic)
49309
+ return false;
49310
+ if (hovering) {
49311
+ this.#setSidePanelHover(topic.id, undefined);
49312
+ return true;
49313
+ }
49314
+ this.#setSidePanelHover(undefined, undefined);
49315
+ if (topic.id !== this.#state.activeTopicId)
49316
+ this.#activateTopic(topic.id);
49317
+ return true;
49318
+ }
49319
+ #setSidePanelHover(topicId, backgroundSessionId) {
49320
+ if (this.#state.sidePanelHoverTopicId === topicId && this.#state.sidePanelHoverBackgroundSessionId === backgroundSessionId) {
49321
+ return;
49322
+ }
49323
+ this.#state = {
49324
+ ...this.#state,
49325
+ sidePanelHoverTopicId: topicId,
49326
+ sidePanelHoverBackgroundSessionId: backgroundSessionId
49327
+ };
49328
+ this.#queueRender();
49329
+ }
49330
+ #highlightPickedTarget(target) {
49331
+ if (target.backgroundSessionId !== undefined) {
49332
+ if (this.#state.topicPickerBackgroundId === target.backgroundSessionId)
49333
+ return true;
49334
+ this.#state = { ...this.#state, topicPickerBackgroundId: target.backgroundSessionId };
49335
+ } else if (target.topicIndex !== undefined) {
49336
+ if (!this.#state.topicPickerBackgroundId && this.#state.topicPickerIndex === target.topicIndex) {
49337
+ return true;
49338
+ }
49339
+ this.#state = {
49340
+ ...this.#state,
49341
+ topicPickerIndex: target.topicIndex,
49342
+ topicPickerBackgroundId: undefined
49343
+ };
49344
+ } else {
49345
+ return false;
49346
+ }
49347
+ this.#queueRender();
49348
+ return true;
49349
+ }
48793
49350
  #handleMouseSelection(event) {
48794
49351
  if ((event.button & 3) !== 0 && event.kind !== "release")
48795
49352
  return;
@@ -59153,4 +59710,4 @@ switch (command) {
59153
59710
  }
59154
59711
  }
59155
59712
 
59156
- //# debugId=CF8B8D72488623B764756E2164756E21
59713
+ //# debugId=A04216FF3BF92ED864756E2164756E21