@wrongstack/tui 1.0.26 → 1.0.27

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/index.js CHANGED
@@ -24887,378 +24887,1014 @@ var require_jsx_runtime = __commonJS({
24887
24887
  }
24888
24888
  });
24889
24889
 
24890
- // src/connections-health.ts
24891
- var connections_health_exports = {};
24892
- __export(connections_health_exports, {
24893
- collectConnectionsHealth: () => collectConnectionsHealth
24890
+ // src/connection-actions.ts
24891
+ var connection_actions_exports = {};
24892
+ __export(connection_actions_exports, {
24893
+ RESTARTABLE_SERVICES: () => RESTARTABLE_SERVICES,
24894
+ executeConnectionAction: () => executeConnectionAction,
24895
+ isEndpointAlive: () => isEndpointAlive,
24896
+ isRestartableService: () => isRestartableService,
24897
+ killChronicleServer: () => killChronicleServer,
24898
+ killCodebaseIndexServer: () => killCodebaseIndexServer,
24899
+ killKanbanServer: () => killKanbanServer,
24900
+ killMailboxServer: () => killMailboxServer,
24901
+ killSageServer: () => killSageServer,
24902
+ killSessionCatalogServer: () => killSessionCatalogServer,
24903
+ restartAllConnectionServices: () => restartAllConnectionServices,
24904
+ waitForShutdown: () => waitForShutdown
24894
24905
  });
24895
- import { createChronicleProjectAccess as createChronicleProjectAccess2 } from "@wrongstack/core/chronicle";
24906
+ import * as net from "node:net";
24896
24907
  import {
24897
- isMailboxProjectServerAvailable as isMailboxProjectServerAvailable2,
24898
- MailboxProjectServerConnection as MailboxProjectServerConnection2
24908
+ ChronicleProjectServerClient,
24909
+ createChronicleProjectAccess,
24910
+ resolveChronicleProjectServerOptions
24911
+ } from "@wrongstack/core/chronicle";
24912
+ import {
24913
+ isMailboxProjectServerAvailable,
24914
+ MailboxProjectServerConnection
24899
24915
  } from "@wrongstack/core/coordination";
24900
- import { SessionCatalogProjectClient as SessionCatalogProjectClient2 } from "@wrongstack/core/session-catalog";
24901
- import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
24902
- import { getKanbanServerConnection as getKanbanServerConnection2 } from "@wrongstack/kanban";
24903
- import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
24904
- import { isSageProjectServerAvailable as isSageProjectServerAvailable2, SageProjectServerConnection as SageProjectServerConnection2 } from "@wrongstack/sage";
24916
+ import { SessionCatalogProjectClient } from "@wrongstack/core/session-catalog";
24905
24917
  import {
24906
- checkCodebaseIndexServerHealth as checkCodebaseIndexServerHealth2,
24907
- getIndexState,
24908
- resolveProjectIndexDaemonAvailability
24918
+ assertDaemonRuntimeVersion,
24919
+ resolveWstackPaths as resolveWstackPaths2,
24920
+ toErrorMessage
24921
+ } from "@wrongstack/core/utils";
24922
+ import {
24923
+ closeKanbanServerConnections,
24924
+ getKanbanServerConnection,
24925
+ isKanbanServerAvailable
24926
+ } from "@wrongstack/kanban";
24927
+ import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
24928
+ import {
24929
+ checkCodebaseIndexServerHealth,
24930
+ ensureCodebaseIndexServer,
24931
+ shutdownCodebaseIndexServer
24909
24932
  } from "@wrongstack/tools";
24910
- async function collectConnectionsHealth(projectRoot) {
24911
- const services = await Promise.all([
24912
- sessionCatalogHealth(projectRoot),
24913
- chronicleHealth(projectRoot),
24914
- codebaseIndexHealth(projectRoot),
24915
- sageHealth(projectRoot),
24916
- kanbanHealth(projectRoot),
24917
- mailboxHealth(projectRoot),
24918
- governanceHealth(projectRoot)
24919
- ]);
24920
- const required = services.filter((s2) => s2.required);
24921
- const overall = required.some((s2) => s2.status === "error") ? "error" : required.some((s2) => s2.status !== "healthy") ? "degraded" : "healthy";
24922
- return { checkedAt: Date.now(), overall, services };
24933
+ function isRestartableService(serviceId) {
24934
+ return RESTARTABLE_SERVICES.includes(serviceId);
24923
24935
  }
24924
- async function sessionCatalogHealth(projectRoot) {
24925
- const startedAt = Date.now();
24926
- try {
24927
- const paths = resolveWstackPaths3({ projectRoot });
24928
- const client = new SessionCatalogProjectClient2({
24929
- projectDir: paths.projectDir,
24930
- projectRoot
24936
+ function isEndpointAlive(endpoint) {
24937
+ return new Promise((resolve) => {
24938
+ const sock = net.createConnection(endpoint);
24939
+ const timer = setTimeout(() => {
24940
+ sock.destroy();
24941
+ resolve(false);
24942
+ }, 500);
24943
+ timer.unref?.();
24944
+ sock.once("connect", () => {
24945
+ clearTimeout(timer);
24946
+ sock.destroy();
24947
+ resolve(true);
24948
+ });
24949
+ sock.once("error", () => {
24950
+ clearTimeout(timer);
24951
+ sock.destroy();
24952
+ resolve(false);
24931
24953
  });
24954
+ });
24955
+ }
24956
+ async function waitForShutdown(probe) {
24957
+ const pollIntervalMs = 250;
24958
+ if (!probe) {
24959
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
24960
+ return;
24961
+ }
24962
+ const deadline = Date.now() + 3e3;
24963
+ while (Date.now() < deadline) {
24932
24964
  try {
24933
- const health2 = await client.callExisting("ping", {}, { timeoutMs: 3e3 });
24965
+ const stillUp = await probe();
24966
+ if (!stillUp) return;
24967
+ } catch {
24968
+ return;
24969
+ }
24970
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
24971
+ }
24972
+ throw new Error("Daemon did not stop before the restart deadline; restart was not verified.");
24973
+ }
24974
+ async function executeConnectionAction(serviceId, action, projectRoot, indexDir) {
24975
+ switch (serviceId) {
24976
+ case "session-catalog":
24977
+ return killSessionCatalogServer(projectRoot, action);
24978
+ case "kanban":
24979
+ return killKanbanServer(projectRoot, action);
24980
+ case "sage":
24981
+ return killSageServer(projectRoot, action);
24982
+ case "chronicle":
24983
+ return killChronicleServer(projectRoot, action);
24984
+ case "codebase-index":
24985
+ return killCodebaseIndexServer(projectRoot, indexDir, action);
24986
+ case "mailbox":
24987
+ return killMailboxServer(projectRoot, action);
24988
+ case "governance":
24934
24989
  return {
24935
- id: "session-catalog",
24936
- label: "Session Catalog",
24937
- status: health2.damagedRows > 0 ? "degraded" : "healthy",
24938
- required: true,
24939
- mode: "project-daemon",
24940
- detail: health2.damagedRows > 0 ? `${health2.damagedRows} damaged catalog row(s); rebuild is required.` : `${health2.catalogRows} catalog session(s), ${health2.liveLeases} live lease(s), ${health2.reservations} reservation(s).`,
24941
- ownerPid: health2.pid,
24942
- endpoint: health2.endpoint,
24943
- storage: health2.databasePath,
24944
- uptimeMs: health2.uptimeMs,
24945
- latencyMs: Date.now() - startedAt,
24946
- clients: health2.clients,
24947
- activeRequests: health2.activeRequests,
24948
- queuedWork: health2.reservations + health2.maintenanceLeases
24990
+ serviceId: "governance",
24991
+ action,
24992
+ success: false,
24993
+ message: "Governance control plane is read-only; daemon restart is not supported."
24994
+ };
24995
+ default:
24996
+ return {
24997
+ serviceId,
24998
+ action,
24999
+ success: false,
25000
+ message: `Unknown service: ${serviceId}`
25001
+ };
25002
+ }
25003
+ }
25004
+ async function restartAllConnectionServices(projectRoot, indexDir) {
25005
+ const results = [];
25006
+ for (const id of RESTARTABLE_SERVICES) {
25007
+ try {
25008
+ results.push(await executeConnectionAction(id, "restart", projectRoot, indexDir));
25009
+ } catch (error) {
25010
+ results.push({
25011
+ serviceId: id,
25012
+ action: "restart",
25013
+ success: false,
25014
+ message: toErrorMessage(error)
25015
+ });
25016
+ }
25017
+ }
25018
+ return results;
25019
+ }
25020
+ async function killSessionCatalogServer(projectRoot, action) {
25021
+ const paths = resolveWstackPaths2({ projectRoot });
25022
+ const client = new SessionCatalogProjectClient({
25023
+ projectDir: paths.projectDir,
25024
+ projectRoot
25025
+ });
25026
+ try {
25027
+ const previousPid = action === "restart" ? await client.callExisting("ping", {}, { timeoutMs: 1e3 }).then(
25028
+ (health2) => health2.pid,
25029
+ () => void 0
25030
+ ) : void 0;
25031
+ const result = await client.shutdown(`TUI request: ${action}`);
25032
+ if (!result.stopped && action === "shutdown") {
25033
+ return {
25034
+ serviceId: "session-catalog",
25035
+ action,
25036
+ success: false,
25037
+ message: "Session Catalog IPC daemon is not running"
24949
25038
  };
24950
- } finally {
24951
- await client.close().catch(() => void 0);
24952
25039
  }
25040
+ if (action === "restart") return await restartSessionCatalogServer(projectRoot, previousPid);
25041
+ return {
25042
+ serviceId: "session-catalog",
25043
+ action,
25044
+ success: true,
25045
+ message: "Session Catalog IPC daemon shutdown requested"
25046
+ };
24953
25047
  } catch (error) {
24954
- const offline = isOfflineConnectionError(error);
24955
25048
  return {
24956
- id: "session-catalog",
24957
- label: "Session Catalog",
24958
- status: offline ? "offline" : "error",
24959
- required: !offline,
24960
- mode: offline ? "on-demand project-daemon" : "project-daemon",
24961
- detail: offline ? "Not running for this project; it starts on demand when a session is used." : "Project-scoped session ownership and catalog are unavailable.",
24962
- latencyMs: Date.now() - startedAt,
24963
- lastError: error instanceof Error ? error.message : String(error)
25049
+ serviceId: "session-catalog",
25050
+ action,
25051
+ success: false,
25052
+ message: toErrorMessage(error)
24964
25053
  };
25054
+ } finally {
25055
+ await client.close().catch(() => void 0);
24965
25056
  }
24966
25057
  }
24967
- async function chronicleHealth(projectRoot) {
24968
- const startedAt = Date.now();
24969
- let access;
25058
+ async function restartSessionCatalogServer(projectRoot, previousPid) {
25059
+ const paths = resolveWstackPaths2({ projectRoot });
25060
+ const probe = () => {
25061
+ const client = new SessionCatalogProjectClient({
25062
+ projectDir: paths.projectDir,
25063
+ projectRoot
25064
+ });
25065
+ return client.callExisting("ping", {}, { timeoutMs: 1e3 }).then(
25066
+ () => true,
25067
+ () => false
25068
+ ).finally(() => client.close().catch(() => void 0));
25069
+ };
25070
+ await waitForShutdown(probe);
25071
+ const verify = new SessionCatalogProjectClient({
25072
+ projectDir: paths.projectDir,
25073
+ projectRoot
25074
+ });
24970
25075
  try {
24971
- access = createChronicleProjectAccess2({ projectRoot });
24972
- const health2 = await access.call("ping", {}, { timeoutMs: 5e3 });
24973
- const quarantined = health2.quarantinedFamilies ?? [];
25076
+ const health2 = await verify.ping();
25077
+ assertDaemonRuntimeVersion(health2.runtimeVersion);
25078
+ if (previousPid !== void 0 && health2.pid === previousPid) {
25079
+ return {
25080
+ serviceId: "session-catalog",
25081
+ action: "restart",
25082
+ success: false,
25083
+ message: `Session Catalog IPC daemon did not restart (owner PID is still ${previousPid})`
25084
+ };
25085
+ }
24974
25086
  return {
24975
- id: "chronicle",
24976
- label: "Chronicle telemetry",
24977
- status: quarantined.length > 0 ? "degraded" : access.mode === "server" ? "healthy" : "degraded",
24978
- required: true,
24979
- mode: access.mode,
24980
- detail: quarantined.length > 0 ? `Serving, but ${quarantined.length} day(s) were quarantined for a broken chain: ${quarantined.map((family) => family.day).join(", ")}.` : access.mode === "server" ? "One project owner collects, processes, stores, and serves telemetry." : "Inline fallback is active; ownership is not shared across clients.",
24981
- ownerPid: health2.pid,
24982
- endpoint: health2.endpoint,
24983
- storage: health2.chronicleDirectory,
24984
- uptimeMs: health2.uptimeMs,
24985
- latencyMs: Date.now() - startedAt,
24986
- clients: health2.clients,
24987
- activeRequests: health2.activeRequests,
24988
- queuedWork: health2.journal.pendingEvents,
24989
- watcher: { active: health2.watcher.active, watchedFiles: health2.watcher.watchedFiles },
24990
- ...quarantined[0] ? { lastError: quarantined[0].reason } : health2.watcher.lastError ? { lastError: health2.watcher.lastError } : {}
25087
+ serviceId: "session-catalog",
25088
+ action: "restart",
25089
+ success: true,
25090
+ message: "Session Catalog IPC daemon restarted successfully"
24991
25091
  };
24992
25092
  } catch (error) {
24993
- return failureService(
24994
- "chronicle",
24995
- "Chronicle telemetry",
24996
- true,
24997
- access?.mode ?? "unavailable",
24998
- error,
24999
- Date.now() - startedAt
25000
- );
25093
+ return {
25094
+ serviceId: "session-catalog",
25095
+ action: "restart",
25096
+ success: false,
25097
+ message: `Session Catalog IPC daemon restarted but verification failed: ${toErrorMessage(error)}`
25098
+ };
25001
25099
  } finally {
25002
- await access?.close();
25100
+ await verify.close().catch(() => void 0);
25003
25101
  }
25004
25102
  }
25005
- async function codebaseIndexHealth(projectRoot) {
25006
- const startedAt = Date.now();
25007
- const availability = resolveProjectIndexDaemonAvailability(projectRoot);
25008
- if (availability.kind === "endpoint-invalid") {
25103
+ async function killKanbanServer(projectRoot, action) {
25104
+ if (process.env["WRONGSTACK_KANBAN_SERVER"] === "0") {
25009
25105
  return {
25010
- id: "codebase-index",
25011
- label: "Codebase index",
25012
- status: "unavailable",
25013
- required: false,
25014
- mode: "endpoint-invalid",
25015
- detail: `Socket path is ${availability.byteLength} bytes \u2014 over this platform's ${availability.maxBytes}-byte sun_path limit. Queries fall back to a process-local index. Set a shorter TMPDIR to restore the shared daemon.`,
25016
- endpoint: availability.endpoint,
25017
- latencyMs: Date.now() - startedAt
25106
+ serviceId: "kanban",
25107
+ action,
25108
+ success: false,
25109
+ message: "Kanban IPC daemon is disabled via WRONGSTACK_KANBAN_SERVER=0"
25018
25110
  };
25019
25111
  }
25112
+ let connection;
25020
25113
  try {
25021
- const health2 = await checkCodebaseIndexServerHealth2(projectRoot, void 0, {
25022
- timeoutMs: 2e3
25023
- });
25024
- const connection = getIndexState().server;
25114
+ connection = await getKanbanServerConnection(projectRoot);
25115
+ } catch (error) {
25025
25116
  return {
25026
- id: "codebase-index",
25027
- label: "Codebase index",
25028
- status: health2.status === "unresponsive" ? "error" : health2.status,
25029
- required: false,
25030
- mode: "project-server",
25031
- detail: health2.server?.activity.indexing ? `Indexing ${health2.server.activity.currentFile}/${health2.server.activity.totalFiles}.` : "Shared symbol index is ready for project queries.",
25032
- ownerPid: connection.pid,
25033
- endpoint: connection.endpoint,
25034
- storage: connection.indexDir ?? resolveWstackPaths3({ projectRoot }).projectCodebaseIndex,
25035
- uptimeMs: health2.server?.uptimeMs,
25036
- latencyMs: health2.latencyMs ?? Date.now() - startedAt,
25037
- clients: health2.server?.clients,
25038
- activeRequests: health2.server?.activeRequests,
25039
- queuedWork: health2.server?.queuedWrites,
25040
- watcher: health2.server ? { active: health2.server.watchingExternal, watchedFiles: health2.server.watchingClients } : void 0
25117
+ serviceId: "kanban",
25118
+ action,
25119
+ success: false,
25120
+ message: toErrorMessage(error)
25041
25121
  };
25042
- } catch (error) {
25043
- if (isOfflineConnectionError(error)) {
25044
- return {
25045
- ...failureService(
25046
- "codebase-index",
25047
- "Codebase index",
25048
- false,
25049
- "on-demand project-server",
25050
- error,
25051
- Date.now() - startedAt
25052
- ),
25053
- status: "offline",
25054
- detail: "Not running for this project; it starts on demand when indexing is used."
25055
- };
25056
- }
25057
- return failureService(
25058
- "codebase-index",
25059
- "Codebase index",
25060
- false,
25061
- "project-server",
25062
- error,
25063
- Date.now() - startedAt
25064
- );
25065
25122
  }
25066
- }
25067
- async function sageHealth(projectRoot) {
25068
- const startedAt = Date.now();
25069
- if (!isSageProjectServerAvailable2()) {
25123
+ if (!connection) {
25070
25124
  return {
25071
- id: "sage",
25072
- label: "SAGE memory",
25073
- status: "unavailable",
25074
- required: false,
25075
- mode: "unavailable",
25076
- detail: "The packaged SAGE project server is unavailable in this runtime."
25125
+ serviceId: "kanban",
25126
+ action,
25127
+ success: false,
25128
+ message: "Kanban IPC daemon is not running"
25077
25129
  };
25078
25130
  }
25079
- const connection = new SageProjectServerConnection2(projectRoot);
25080
25131
  try {
25081
- const status = await connection.status();
25082
- if (!status) {
25132
+ const result = await connection.request("shutdown", {
25133
+ reason: `TUI request: ${action}`
25134
+ });
25135
+ if (!result.stopping) {
25083
25136
  return {
25084
- id: "sage",
25085
- label: "SAGE memory",
25086
- status: "offline",
25087
- required: false,
25088
- mode: "on-demand project-server",
25089
- detail: "Not running for this project; it starts on demand when memory is used.",
25090
- endpoint: connection.getState().endpoint,
25091
- latencyMs: Date.now() - startedAt
25137
+ serviceId: "kanban",
25138
+ action,
25139
+ success: false,
25140
+ message: "Kanban IPC daemon shutdown failed (not confirmed)"
25092
25141
  };
25093
25142
  }
25094
- const healthStatus = status.health.status === "ready" ? "healthy" : status.health.status === "degraded" ? "degraded" : "error";
25143
+ if (action === "restart") {
25144
+ closeKanbanServerConnections();
25145
+ return await restartKanbanServer(projectRoot);
25146
+ }
25095
25147
  return {
25096
- id: "sage",
25097
- label: "SAGE memory",
25098
- status: healthStatus,
25099
- required: false,
25100
- mode: "project-server",
25101
- detail: `Persistent project memory backend: ${status.health.backend}.`,
25102
- ownerPid: status.pid,
25103
- endpoint: status.endpoint,
25104
- storage: status.storageRoot,
25105
- latencyMs: Date.now() - startedAt,
25106
- clients: status.clients,
25107
- activeRequests: status.pendingRequests
25148
+ serviceId: "kanban",
25149
+ action,
25150
+ success: true,
25151
+ message: "Kanban IPC daemon shutdown requested"
25108
25152
  };
25109
25153
  } catch (error) {
25110
- return failureService(
25111
- "sage",
25112
- "SAGE memory",
25113
- false,
25114
- "project-server",
25115
- error,
25116
- Date.now() - startedAt
25117
- );
25118
- } finally {
25119
- connection.close();
25120
- }
25121
- }
25122
- async function kanbanHealth(projectRoot) {
25123
- const startedAt = Date.now();
25124
- if (process.env["WRONGSTACK_KANBAN_SERVER"] === "0") {
25125
25154
  return {
25126
- id: "kanban",
25127
- label: "Kanban IPC",
25128
- status: "unavailable",
25129
- required: false,
25130
- mode: "disabled",
25131
- detail: "Kanban IPC daemon is disabled via WRONGSTACK_KANBAN_SERVER=0."
25155
+ serviceId: "kanban",
25156
+ action,
25157
+ success: false,
25158
+ message: toErrorMessage(error)
25132
25159
  };
25133
25160
  }
25134
- let connection;
25161
+ }
25162
+ async function restartKanbanServer(projectRoot) {
25163
+ await waitForShutdown(() => isKanbanServerAvailable(projectRoot));
25135
25164
  try {
25136
- connection = await getKanbanServerConnection2(projectRoot);
25137
- } catch (error) {
25138
- return failureService(
25139
- "kanban",
25140
- "Kanban IPC",
25141
- false,
25142
- "project-server",
25143
- error,
25144
- Date.now() - startedAt
25145
- );
25146
- }
25147
- if (!connection) {
25165
+ const connection = await getKanbanServerConnection(projectRoot);
25166
+ if (!connection) {
25167
+ return {
25168
+ serviceId: "kanban",
25169
+ action: "restart",
25170
+ success: false,
25171
+ message: "Kanban IPC daemon failed to restart (no connection after re-init)"
25172
+ };
25173
+ }
25174
+ const health2 = await connection.request("ping", {}, { timeoutMs: 1e4 });
25175
+ assertDaemonRuntimeVersion(health2.runtimeVersion);
25148
25176
  return {
25149
- id: "kanban",
25150
- label: "Kanban IPC",
25151
- status: "unavailable",
25152
- required: false,
25153
- mode: "disabled",
25154
- detail: "Kanban IPC daemon is disabled in this runtime."
25177
+ serviceId: "kanban",
25178
+ action: "restart",
25179
+ success: true,
25180
+ message: "Kanban IPC daemon restarted successfully"
25155
25181
  };
25156
- }
25157
- try {
25158
- const status = await connection.request("ping", {}, { timeoutMs: 5e3 });
25182
+ } catch (error) {
25159
25183
  return {
25160
- id: "kanban",
25161
- label: "Kanban IPC",
25162
- status: "healthy",
25163
- required: true,
25164
- mode: "project-server",
25165
- detail: `Single shared project-server owns kanban state (v${status.protocolVersion}).`,
25166
- ownerPid: status.pid,
25167
- endpoint: status.endpoint,
25168
- storage: status.databasePath,
25169
- latencyMs: Date.now() - startedAt,
25170
- clients: status.clients,
25171
- activeRequests: status.pendingRequests,
25172
- uptimeMs: Date.now() - new Date(status.startedAt).getTime()
25184
+ serviceId: "kanban",
25185
+ action: "restart",
25186
+ success: false,
25187
+ message: `Kanban IPC daemon restarted but verification failed: ${toErrorMessage(error)}`
25173
25188
  };
25174
- } catch (error) {
25175
- return failureService(
25176
- "kanban",
25177
- "Kanban IPC",
25178
- true,
25179
- "project-server",
25180
- error,
25181
- Date.now() - startedAt
25182
- );
25183
25189
  }
25184
25190
  }
25185
- async function mailboxHealth(projectRoot) {
25186
- const startedAt = Date.now();
25187
- if (!isMailboxProjectServerAvailable2()) {
25191
+ async function killSageServer(projectRoot, action) {
25192
+ if (!isSageProjectServerAvailable()) {
25188
25193
  return {
25189
- id: "mailbox",
25190
- label: "Mailbox IPC",
25191
- status: "unavailable",
25192
- required: false,
25193
- mode: "unavailable",
25194
- detail: "The packaged mailbox project server is unavailable in this runtime."
25194
+ serviceId: "sage",
25195
+ action,
25196
+ success: false,
25197
+ message: "SAGE project server is unavailable in this runtime"
25195
25198
  };
25196
25199
  }
25197
- const connection = new MailboxProjectServerConnection2(
25198
- resolveWstackPaths3({ projectRoot }).projectDir
25199
- );
25200
+ const connection = new SageProjectServerConnection(projectRoot);
25200
25201
  try {
25201
- const status = await connection.probeStatus();
25202
- if (!status) {
25202
+ const result = await connection.shutdown(`TUI request: ${action}`);
25203
+ if (!result.stopped) {
25204
+ if (action === "restart" && result.reason === "not-running") {
25205
+ return await restartSageServer(projectRoot);
25206
+ }
25203
25207
  return {
25204
- id: "mailbox",
25205
- label: "Mailbox IPC",
25206
- status: "offline",
25207
- required: false,
25208
- mode: "on-demand project-server",
25209
- detail: "Not running for this project; it starts on demand when mailbox is used.",
25210
- endpoint: connection.getState().endpoint,
25211
- latencyMs: Date.now() - startedAt
25208
+ serviceId: "sage",
25209
+ action,
25210
+ success: false,
25211
+ message: `SAGE memory server shutdown failed: ${result.reason ?? "unknown"}`
25212
25212
  };
25213
25213
  }
25214
+ if (action === "restart") {
25215
+ return await restartSageServer(projectRoot);
25216
+ }
25214
25217
  return {
25215
- id: "mailbox",
25216
- label: "Mailbox IPC",
25217
- status: "healthy",
25218
- required: true,
25219
- mode: "project-server",
25220
- detail: `Single shared project-server owns inter-agent mailbox state (v${status.protocolVersion}).`,
25221
- ownerPid: status.pid,
25222
- endpoint: status.endpoint,
25223
- storage: status.databasePath,
25224
- latencyMs: Date.now() - startedAt,
25225
- clients: status.clients,
25226
- activeRequests: status.pendingRequests,
25227
- uptimeMs: Date.now() - new Date(status.startedAt).getTime()
25218
+ serviceId: "sage",
25219
+ action,
25220
+ success: true,
25221
+ message: "SAGE memory server shutdown requested"
25228
25222
  };
25229
25223
  } catch (error) {
25230
- return failureService(
25231
- "mailbox",
25232
- "Mailbox IPC",
25233
- true,
25234
- "project-server",
25235
- error,
25236
- Date.now() - startedAt
25237
- );
25224
+ return {
25225
+ serviceId: "sage",
25226
+ action,
25227
+ success: false,
25228
+ message: toErrorMessage(error)
25229
+ };
25238
25230
  } finally {
25239
25231
  connection.close();
25240
25232
  }
25241
25233
  }
25242
- async function governanceHealth(projectRoot) {
25243
- const startedAt = Date.now();
25244
- const result = await readGovernanceDaemonOperatorStatus(projectRoot);
25245
- if (!result.available) {
25246
- const missing = result.code === "broker_missing";
25234
+ async function restartSageServer(projectRoot) {
25235
+ await waitForShutdown(async () => {
25236
+ const probe = new SageProjectServerConnection(projectRoot);
25237
+ try {
25238
+ return await probe.status() !== null;
25239
+ } finally {
25240
+ probe.close();
25241
+ }
25242
+ });
25243
+ const verifyConn = new SageProjectServerConnection(projectRoot);
25244
+ try {
25245
+ const health2 = await verifyConn.call(
25246
+ "ping",
25247
+ {},
25248
+ { timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } }
25249
+ );
25250
+ assertDaemonRuntimeVersion(health2.runtimeVersion);
25247
25251
  return {
25248
- id: "governance",
25249
- label: "Governance control plane",
25250
- status: missing ? "offline" : "error",
25251
- required: false,
25252
- mode: missing ? "compatibility-default-off" : "project-daemon",
25253
- detail: missing ? "Not active for this project. Existing agent and model execution remains unchanged." : `Read-only governance status is unavailable: ${result.message}`,
25254
- latencyMs: Date.now() - startedAt,
25255
- control: "none",
25256
- ...missing ? {} : { lastError: result.message }
25252
+ serviceId: "sage",
25253
+ action: "restart",
25254
+ success: true,
25255
+ message: "SAGE memory server restarted successfully"
25257
25256
  };
25258
- }
25259
- const { status } = result;
25260
- return {
25261
- id: "governance",
25257
+ } catch (error) {
25258
+ return {
25259
+ serviceId: "sage",
25260
+ action: "restart",
25261
+ success: false,
25262
+ message: `SAGE memory server restarted but verification failed: ${toErrorMessage(error)}`
25263
+ };
25264
+ } finally {
25265
+ verifyConn.close();
25266
+ }
25267
+ }
25268
+ async function killChronicleServer(projectRoot, action) {
25269
+ const options = resolveChronicleProjectServerOptions({ projectRoot });
25270
+ const client = new ChronicleProjectServerClient(options);
25271
+ try {
25272
+ const result = await client.shutdown(`TUI request: ${action}`);
25273
+ if (!result.stopped) {
25274
+ if (action === "restart" && result.reason === "offline") {
25275
+ return await restartChronicleServer(projectRoot);
25276
+ }
25277
+ return {
25278
+ serviceId: "chronicle",
25279
+ action,
25280
+ success: false,
25281
+ message: `Chronicle telemetry server shutdown failed: ${result.reason ?? "unknown"}`
25282
+ };
25283
+ }
25284
+ if (action === "restart") {
25285
+ return await restartChronicleServer(projectRoot);
25286
+ }
25287
+ return {
25288
+ serviceId: "chronicle",
25289
+ action,
25290
+ success: true,
25291
+ message: "Chronicle telemetry server shutdown requested"
25292
+ };
25293
+ } catch (error) {
25294
+ return {
25295
+ serviceId: "chronicle",
25296
+ action,
25297
+ success: false,
25298
+ message: toErrorMessage(error)
25299
+ };
25300
+ } finally {
25301
+ client.close();
25302
+ }
25303
+ }
25304
+ async function restartChronicleServer(projectRoot) {
25305
+ const options = resolveChronicleProjectServerOptions({ projectRoot });
25306
+ const endpoint = new ChronicleProjectServerClient(options).endpoint;
25307
+ await waitForShutdown(async () => isEndpointAlive(endpoint));
25308
+ let access;
25309
+ try {
25310
+ access = createChronicleProjectAccess({ projectRoot });
25311
+ const health2 = await access.call("ping", {}, { timeoutMs: 1e4 });
25312
+ if (access.mode !== "server") {
25313
+ return {
25314
+ serviceId: "chronicle",
25315
+ action: "restart",
25316
+ success: false,
25317
+ message: `Chronicle telemetry server restarted but running in ${access.mode} mode (expected server)`
25318
+ };
25319
+ }
25320
+ assertDaemonRuntimeVersion(health2.runtimeVersion);
25321
+ return {
25322
+ serviceId: "chronicle",
25323
+ action: "restart",
25324
+ success: true,
25325
+ message: "Chronicle telemetry server restarted successfully"
25326
+ };
25327
+ } catch (error) {
25328
+ return {
25329
+ serviceId: "chronicle",
25330
+ action: "restart",
25331
+ success: false,
25332
+ message: `Chronicle telemetry server restarted but verification failed: ${toErrorMessage(error)}`
25333
+ };
25334
+ } finally {
25335
+ await access?.close();
25336
+ }
25337
+ }
25338
+ async function killCodebaseIndexServer(projectRoot, indexDir, action) {
25339
+ try {
25340
+ const result = await shutdownCodebaseIndexServer(
25341
+ projectRoot,
25342
+ indexDir,
25343
+ `tui-request:${action}`
25344
+ );
25345
+ if (!result.stopped) {
25346
+ if (action === "restart" && result.reason === "not-running") {
25347
+ return await restartCodebaseIndexServer(projectRoot, indexDir);
25348
+ }
25349
+ return {
25350
+ serviceId: "codebase-index",
25351
+ action,
25352
+ success: false,
25353
+ message: `Codebase index server shutdown failed: ${result.reason ?? "unknown"}`
25354
+ };
25355
+ }
25356
+ if (action === "restart") {
25357
+ return await restartCodebaseIndexServer(projectRoot, indexDir);
25358
+ }
25359
+ return {
25360
+ serviceId: "codebase-index",
25361
+ action,
25362
+ success: true,
25363
+ message: "Codebase index server shutdown requested"
25364
+ };
25365
+ } catch (error) {
25366
+ return {
25367
+ serviceId: "codebase-index",
25368
+ action,
25369
+ success: false,
25370
+ message: toErrorMessage(error)
25371
+ };
25372
+ }
25373
+ }
25374
+ async function restartCodebaseIndexServer(projectRoot, indexDir) {
25375
+ await waitForShutdown(async () => {
25376
+ try {
25377
+ await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
25378
+ timeoutMs: 1e3
25379
+ });
25380
+ return true;
25381
+ } catch {
25382
+ return false;
25383
+ }
25384
+ });
25385
+ try {
25386
+ await ensureCodebaseIndexServer({ projectRoot, indexDir });
25387
+ const health2 = await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
25388
+ timeoutMs: 1e4
25389
+ });
25390
+ if (health2.status !== "healthy") {
25391
+ return {
25392
+ serviceId: "codebase-index",
25393
+ action: "restart",
25394
+ success: false,
25395
+ message: `Codebase index server restart verification failed: ${health2.status}`
25396
+ };
25397
+ }
25398
+ assertDaemonRuntimeVersion(health2.server?.runtimeVersion);
25399
+ return {
25400
+ serviceId: "codebase-index",
25401
+ action: "restart",
25402
+ success: true,
25403
+ message: "Codebase index server restarted successfully"
25404
+ };
25405
+ } catch (error) {
25406
+ return {
25407
+ serviceId: "codebase-index",
25408
+ action: "restart",
25409
+ success: false,
25410
+ message: `Codebase index server restarted but verification failed: ${toErrorMessage(error)}`
25411
+ };
25412
+ }
25413
+ }
25414
+ async function killMailboxServer(projectRoot, action) {
25415
+ if (!isMailboxProjectServerAvailable()) {
25416
+ return {
25417
+ serviceId: "mailbox",
25418
+ action,
25419
+ success: false,
25420
+ message: "Mailbox project server is unavailable in this runtime"
25421
+ };
25422
+ }
25423
+ const connection = new MailboxProjectServerConnection(
25424
+ resolveWstackPaths2({ projectRoot }).projectDir
25425
+ );
25426
+ try {
25427
+ const result = await connection.shutdown(`TUI request: ${action}`);
25428
+ if (!result.stopped) {
25429
+ if (action === "restart" && result.reason === "offline") {
25430
+ return await restartMailboxServer(projectRoot);
25431
+ }
25432
+ return {
25433
+ serviceId: "mailbox",
25434
+ action,
25435
+ success: false,
25436
+ message: `Mailbox IPC server shutdown failed: ${result.reason ?? "unknown"}`
25437
+ };
25438
+ }
25439
+ if (action === "restart") {
25440
+ return await restartMailboxServer(projectRoot);
25441
+ }
25442
+ return {
25443
+ serviceId: "mailbox",
25444
+ action,
25445
+ success: true,
25446
+ message: "Mailbox IPC server shutdown requested"
25447
+ };
25448
+ } catch (error) {
25449
+ return {
25450
+ serviceId: "mailbox",
25451
+ action,
25452
+ success: false,
25453
+ message: toErrorMessage(error)
25454
+ };
25455
+ } finally {
25456
+ connection.close();
25457
+ }
25458
+ }
25459
+ async function restartMailboxServer(projectRoot) {
25460
+ await waitForShutdown(async () => {
25461
+ const probe = new MailboxProjectServerConnection(
25462
+ resolveWstackPaths2({ projectRoot }).projectDir
25463
+ );
25464
+ try {
25465
+ return await probe.probeStatus() !== null;
25466
+ } finally {
25467
+ probe.close();
25468
+ }
25469
+ });
25470
+ const verifyConn = new MailboxProjectServerConnection(
25471
+ resolveWstackPaths2({ projectRoot }).projectDir
25472
+ );
25473
+ try {
25474
+ const health2 = await verifyConn.call("ping", {}, { timeoutMs: 1e4 });
25475
+ assertDaemonRuntimeVersion(health2.runtimeVersion);
25476
+ return {
25477
+ serviceId: "mailbox",
25478
+ action: "restart",
25479
+ success: true,
25480
+ message: "Mailbox IPC server restarted successfully"
25481
+ };
25482
+ } catch (error) {
25483
+ return {
25484
+ serviceId: "mailbox",
25485
+ action: "restart",
25486
+ success: false,
25487
+ message: `Mailbox IPC server restarted but verification failed: ${toErrorMessage(error)}`
25488
+ };
25489
+ } finally {
25490
+ verifyConn.close();
25491
+ }
25492
+ }
25493
+ var RESTARTABLE_SERVICES;
25494
+ var init_connection_actions = __esm({
25495
+ "src/connection-actions.ts"() {
25496
+ "use strict";
25497
+ RESTARTABLE_SERVICES = [
25498
+ "session-catalog",
25499
+ "chronicle",
25500
+ "codebase-index",
25501
+ "sage",
25502
+ "kanban",
25503
+ "mailbox"
25504
+ ];
25505
+ }
25506
+ });
25507
+
25508
+ // src/connections-health.ts
25509
+ var connections_health_exports = {};
25510
+ __export(connections_health_exports, {
25511
+ collectConnectionsHealth: () => collectConnectionsHealth
25512
+ });
25513
+ import { createChronicleProjectAccess as createChronicleProjectAccess2 } from "@wrongstack/core/chronicle";
25514
+ import {
25515
+ isMailboxProjectServerAvailable as isMailboxProjectServerAvailable2,
25516
+ MailboxProjectServerConnection as MailboxProjectServerConnection2
25517
+ } from "@wrongstack/core/coordination";
25518
+ import { SessionCatalogProjectClient as SessionCatalogProjectClient2 } from "@wrongstack/core/session-catalog";
25519
+ import { resolveWstackPaths as resolveWstackPaths3, withDaemonVersion } from "@wrongstack/core/utils";
25520
+ import { getKanbanServerConnection as getKanbanServerConnection2 } from "@wrongstack/kanban";
25521
+ import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
25522
+ import { isSageProjectServerAvailable as isSageProjectServerAvailable2, SageProjectServerConnection as SageProjectServerConnection2 } from "@wrongstack/sage";
25523
+ import {
25524
+ checkCodebaseIndexServerHealth as checkCodebaseIndexServerHealth2,
25525
+ getIndexState,
25526
+ resolveProjectIndexDaemonAvailability
25527
+ } from "@wrongstack/tools";
25528
+ async function collectConnectionsHealth(projectRoot) {
25529
+ const services = await Promise.all([
25530
+ sessionCatalogHealth(projectRoot),
25531
+ chronicleHealth(projectRoot),
25532
+ codebaseIndexHealth(projectRoot),
25533
+ sageHealth(projectRoot),
25534
+ kanbanHealth(projectRoot),
25535
+ mailboxHealth(projectRoot),
25536
+ governanceHealth(projectRoot)
25537
+ ]);
25538
+ const required = services.filter((s2) => s2.required);
25539
+ const overall = required.some((s2) => s2.status === "error") ? "error" : services.some((s2) => s2.versionMismatch) || required.some((s2) => s2.status !== "healthy") ? "degraded" : "healthy";
25540
+ return { checkedAt: Date.now(), overall, services };
25541
+ }
25542
+ async function sessionCatalogHealth(projectRoot) {
25543
+ const startedAt = Date.now();
25544
+ try {
25545
+ const paths = resolveWstackPaths3({ projectRoot });
25546
+ const client = new SessionCatalogProjectClient2({
25547
+ projectDir: paths.projectDir,
25548
+ projectRoot
25549
+ });
25550
+ try {
25551
+ const health2 = await client.callExisting("ping", {}, { timeoutMs: 3e3 });
25552
+ return withDaemonVersion(
25553
+ {
25554
+ id: "session-catalog",
25555
+ label: "Session Catalog",
25556
+ status: health2.damagedRows > 0 ? "degraded" : "healthy",
25557
+ required: true,
25558
+ mode: "project-daemon",
25559
+ detail: health2.damagedRows > 0 ? `${health2.damagedRows} damaged catalog row(s); rebuild is required.` : `${health2.catalogRows} catalog session(s), ${health2.liveLeases} live lease(s), ${health2.reservations} reservation(s).`,
25560
+ ownerPid: health2.pid,
25561
+ endpoint: health2.endpoint,
25562
+ storage: health2.databasePath,
25563
+ uptimeMs: health2.uptimeMs,
25564
+ latencyMs: Date.now() - startedAt,
25565
+ clients: health2.clients,
25566
+ activeRequests: health2.activeRequests,
25567
+ queuedWork: health2.reservations + health2.maintenanceLeases
25568
+ },
25569
+ health2?.runtimeVersion
25570
+ );
25571
+ } finally {
25572
+ await client.close().catch(() => void 0);
25573
+ }
25574
+ } catch (error) {
25575
+ const offline = isOfflineConnectionError(error);
25576
+ return {
25577
+ id: "session-catalog",
25578
+ label: "Session Catalog",
25579
+ status: offline ? "offline" : "error",
25580
+ required: !offline,
25581
+ mode: offline ? "on-demand project-daemon" : "project-daemon",
25582
+ detail: offline ? "Not running for this project; it starts on demand when a session is used." : "Project-scoped session ownership and catalog are unavailable.",
25583
+ latencyMs: Date.now() - startedAt,
25584
+ lastError: error instanceof Error ? error.message : String(error)
25585
+ };
25586
+ }
25587
+ }
25588
+ async function chronicleHealth(projectRoot) {
25589
+ const startedAt = Date.now();
25590
+ let access;
25591
+ try {
25592
+ access = createChronicleProjectAccess2({ projectRoot });
25593
+ const health2 = await access.call("ping", {}, { timeoutMs: 5e3 });
25594
+ const quarantined = health2.quarantinedFamilies ?? [];
25595
+ return withDaemonVersion(
25596
+ {
25597
+ id: "chronicle",
25598
+ label: "Chronicle telemetry",
25599
+ status: quarantined.length > 0 ? "degraded" : access.mode === "server" ? "healthy" : "degraded",
25600
+ required: true,
25601
+ mode: access.mode,
25602
+ detail: quarantined.length > 0 ? `Serving, but ${quarantined.length} day(s) were quarantined for a broken chain: ${quarantined.map((family) => family.day).join(", ")}.` : access.mode === "server" ? "One project owner collects, processes, stores, and serves telemetry." : "Inline fallback is active; ownership is not shared across clients.",
25603
+ ownerPid: health2.pid,
25604
+ endpoint: health2.endpoint,
25605
+ storage: health2.chronicleDirectory,
25606
+ uptimeMs: health2.uptimeMs,
25607
+ latencyMs: Date.now() - startedAt,
25608
+ clients: health2.clients,
25609
+ activeRequests: health2.activeRequests,
25610
+ queuedWork: health2.journal.pendingEvents,
25611
+ watcher: { active: health2.watcher.active, watchedFiles: health2.watcher.watchedFiles },
25612
+ ...quarantined[0] ? { lastError: quarantined[0].reason } : health2.watcher.lastError ? { lastError: health2.watcher.lastError } : {}
25613
+ },
25614
+ health2?.runtimeVersion
25615
+ );
25616
+ } catch (error) {
25617
+ return failureService(
25618
+ "chronicle",
25619
+ "Chronicle telemetry",
25620
+ true,
25621
+ access?.mode ?? "unavailable",
25622
+ error,
25623
+ Date.now() - startedAt
25624
+ );
25625
+ } finally {
25626
+ await access?.close();
25627
+ }
25628
+ }
25629
+ async function codebaseIndexHealth(projectRoot) {
25630
+ const startedAt = Date.now();
25631
+ const availability = resolveProjectIndexDaemonAvailability(projectRoot);
25632
+ if (availability.kind === "endpoint-invalid") {
25633
+ return {
25634
+ id: "codebase-index",
25635
+ label: "Codebase index",
25636
+ status: "unavailable",
25637
+ required: false,
25638
+ mode: "endpoint-invalid",
25639
+ detail: `Socket path is ${availability.byteLength} bytes \u2014 over this platform's ${availability.maxBytes}-byte sun_path limit. Queries fall back to a process-local index. Set a shorter TMPDIR to restore the shared daemon.`,
25640
+ endpoint: availability.endpoint,
25641
+ latencyMs: Date.now() - startedAt
25642
+ };
25643
+ }
25644
+ try {
25645
+ const health2 = await checkCodebaseIndexServerHealth2(projectRoot, void 0, {
25646
+ timeoutMs: 2e3
25647
+ });
25648
+ const connection = getIndexState().server;
25649
+ return withDaemonVersion(
25650
+ {
25651
+ id: "codebase-index",
25652
+ label: "Codebase index",
25653
+ status: health2.status === "unresponsive" ? "error" : health2.status,
25654
+ required: false,
25655
+ mode: "project-server",
25656
+ detail: health2.server?.activity.indexing ? `Indexing ${health2.server.activity.currentFile}/${health2.server.activity.totalFiles}.` : "Shared symbol index is ready for project queries.",
25657
+ ownerPid: connection.pid,
25658
+ endpoint: connection.endpoint,
25659
+ storage: connection.indexDir ?? resolveWstackPaths3({ projectRoot }).projectCodebaseIndex,
25660
+ uptimeMs: health2.server?.uptimeMs,
25661
+ latencyMs: health2.latencyMs ?? Date.now() - startedAt,
25662
+ clients: health2.server?.clients,
25663
+ activeRequests: health2.server?.activeRequests,
25664
+ queuedWork: health2.server?.queuedWrites,
25665
+ watcher: health2.server ? { active: health2.server.watchingExternal, watchedFiles: health2.server.watchingClients } : void 0
25666
+ },
25667
+ health2.server?.runtimeVersion
25668
+ );
25669
+ } catch (error) {
25670
+ if (isOfflineConnectionError(error)) {
25671
+ return {
25672
+ ...failureService(
25673
+ "codebase-index",
25674
+ "Codebase index",
25675
+ false,
25676
+ "on-demand project-server",
25677
+ error,
25678
+ Date.now() - startedAt
25679
+ ),
25680
+ status: "offline",
25681
+ detail: "Not running for this project; it starts on demand when indexing is used."
25682
+ };
25683
+ }
25684
+ return failureService(
25685
+ "codebase-index",
25686
+ "Codebase index",
25687
+ false,
25688
+ "project-server",
25689
+ error,
25690
+ Date.now() - startedAt
25691
+ );
25692
+ }
25693
+ }
25694
+ async function sageHealth(projectRoot) {
25695
+ const startedAt = Date.now();
25696
+ if (!isSageProjectServerAvailable2()) {
25697
+ return {
25698
+ id: "sage",
25699
+ label: "SAGE memory",
25700
+ status: "unavailable",
25701
+ required: false,
25702
+ mode: "unavailable",
25703
+ detail: "The packaged SAGE project server is unavailable in this runtime."
25704
+ };
25705
+ }
25706
+ const connection = new SageProjectServerConnection2(projectRoot);
25707
+ try {
25708
+ const status = await connection.status();
25709
+ if (!status) {
25710
+ return {
25711
+ id: "sage",
25712
+ label: "SAGE memory",
25713
+ status: "offline",
25714
+ required: false,
25715
+ mode: "on-demand project-server",
25716
+ detail: "Not running for this project; it starts on demand when memory is used.",
25717
+ endpoint: connection.getState().endpoint,
25718
+ latencyMs: Date.now() - startedAt
25719
+ };
25720
+ }
25721
+ const healthStatus = status.health.status === "ready" ? "healthy" : status.health.status === "degraded" ? "degraded" : "error";
25722
+ return withDaemonVersion(
25723
+ {
25724
+ id: "sage",
25725
+ label: "SAGE memory",
25726
+ status: healthStatus,
25727
+ required: false,
25728
+ mode: "project-server",
25729
+ detail: `Persistent project memory backend: ${status.health.backend}.`,
25730
+ ownerPid: status.pid,
25731
+ endpoint: status.endpoint,
25732
+ storage: status.storageRoot,
25733
+ latencyMs: Date.now() - startedAt,
25734
+ clients: status.clients,
25735
+ activeRequests: status.pendingRequests
25736
+ },
25737
+ status?.runtimeVersion
25738
+ );
25739
+ } catch (error) {
25740
+ return failureService(
25741
+ "sage",
25742
+ "SAGE memory",
25743
+ false,
25744
+ "project-server",
25745
+ error,
25746
+ Date.now() - startedAt
25747
+ );
25748
+ } finally {
25749
+ connection.close();
25750
+ }
25751
+ }
25752
+ async function kanbanHealth(projectRoot) {
25753
+ const startedAt = Date.now();
25754
+ if (process.env["WRONGSTACK_KANBAN_SERVER"] === "0") {
25755
+ return {
25756
+ id: "kanban",
25757
+ label: "Kanban IPC",
25758
+ status: "unavailable",
25759
+ required: false,
25760
+ mode: "disabled",
25761
+ detail: "Kanban IPC daemon is disabled via WRONGSTACK_KANBAN_SERVER=0."
25762
+ };
25763
+ }
25764
+ let connection;
25765
+ try {
25766
+ connection = await getKanbanServerConnection2(projectRoot);
25767
+ } catch (error) {
25768
+ return failureService(
25769
+ "kanban",
25770
+ "Kanban IPC",
25771
+ false,
25772
+ "project-server",
25773
+ error,
25774
+ Date.now() - startedAt
25775
+ );
25776
+ }
25777
+ if (!connection) {
25778
+ return {
25779
+ id: "kanban",
25780
+ label: "Kanban IPC",
25781
+ status: "unavailable",
25782
+ required: false,
25783
+ mode: "disabled",
25784
+ detail: "Kanban IPC daemon is disabled in this runtime."
25785
+ };
25786
+ }
25787
+ try {
25788
+ const status = await connection.request("ping", {}, { timeoutMs: 5e3 });
25789
+ return withDaemonVersion(
25790
+ {
25791
+ id: "kanban",
25792
+ label: "Kanban IPC",
25793
+ status: "healthy",
25794
+ required: true,
25795
+ mode: "project-server",
25796
+ detail: `Single shared project-server owns kanban state (v${status.protocolVersion}).`,
25797
+ ownerPid: status.pid,
25798
+ endpoint: status.endpoint,
25799
+ storage: status.databasePath,
25800
+ latencyMs: Date.now() - startedAt,
25801
+ clients: status.clients,
25802
+ activeRequests: status.pendingRequests,
25803
+ uptimeMs: Date.now() - new Date(status.startedAt).getTime()
25804
+ },
25805
+ status?.runtimeVersion
25806
+ );
25807
+ } catch (error) {
25808
+ return failureService(
25809
+ "kanban",
25810
+ "Kanban IPC",
25811
+ true,
25812
+ "project-server",
25813
+ error,
25814
+ Date.now() - startedAt
25815
+ );
25816
+ }
25817
+ }
25818
+ async function mailboxHealth(projectRoot) {
25819
+ const startedAt = Date.now();
25820
+ if (!isMailboxProjectServerAvailable2()) {
25821
+ return {
25822
+ id: "mailbox",
25823
+ label: "Mailbox IPC",
25824
+ status: "unavailable",
25825
+ required: false,
25826
+ mode: "unavailable",
25827
+ detail: "The packaged mailbox project server is unavailable in this runtime."
25828
+ };
25829
+ }
25830
+ const connection = new MailboxProjectServerConnection2(
25831
+ resolveWstackPaths3({ projectRoot }).projectDir
25832
+ );
25833
+ try {
25834
+ const status = await connection.probeStatus();
25835
+ if (!status) {
25836
+ return {
25837
+ id: "mailbox",
25838
+ label: "Mailbox IPC",
25839
+ status: "offline",
25840
+ required: false,
25841
+ mode: "on-demand project-server",
25842
+ detail: "Not running for this project; it starts on demand when mailbox is used.",
25843
+ endpoint: connection.getState().endpoint,
25844
+ latencyMs: Date.now() - startedAt
25845
+ };
25846
+ }
25847
+ return withDaemonVersion(
25848
+ {
25849
+ id: "mailbox",
25850
+ label: "Mailbox IPC",
25851
+ status: "healthy",
25852
+ required: true,
25853
+ mode: "project-server",
25854
+ detail: `Single shared project-server owns inter-agent mailbox state (v${status.protocolVersion}).`,
25855
+ ownerPid: status.pid,
25856
+ endpoint: status.endpoint,
25857
+ storage: status.databasePath,
25858
+ latencyMs: Date.now() - startedAt,
25859
+ clients: status.clients,
25860
+ activeRequests: status.pendingRequests,
25861
+ uptimeMs: Date.now() - new Date(status.startedAt).getTime()
25862
+ },
25863
+ status?.runtimeVersion
25864
+ );
25865
+ } catch (error) {
25866
+ return failureService(
25867
+ "mailbox",
25868
+ "Mailbox IPC",
25869
+ true,
25870
+ "project-server",
25871
+ error,
25872
+ Date.now() - startedAt
25873
+ );
25874
+ } finally {
25875
+ connection.close();
25876
+ }
25877
+ }
25878
+ async function governanceHealth(projectRoot) {
25879
+ const startedAt = Date.now();
25880
+ const result = await readGovernanceDaemonOperatorStatus(projectRoot);
25881
+ if (!result.available) {
25882
+ const missing = result.code === "broker_missing";
25883
+ return {
25884
+ id: "governance",
25885
+ label: "Governance control plane",
25886
+ status: missing ? "offline" : "error",
25887
+ required: false,
25888
+ mode: missing ? "compatibility-default-off" : "project-daemon",
25889
+ detail: missing ? "Not active for this project. Existing agent and model execution remains unchanged." : `Read-only governance status is unavailable: ${result.message}`,
25890
+ latencyMs: Date.now() - startedAt,
25891
+ control: "none",
25892
+ ...missing ? {} : { lastError: result.message }
25893
+ };
25894
+ }
25895
+ const { status } = result;
25896
+ return {
25897
+ id: "governance",
25262
25898
  label: "Governance control plane",
25263
25899
  status: status.signal.level === "healthy" ? "healthy" : "degraded",
25264
25900
  required: false,
@@ -34210,6 +34846,23 @@ function parseHexColor(hex) {
34210
34846
  const n = Number.parseInt(raw, 16);
34211
34847
  return [n >> 16 & 255, n >> 8 & 255, n & 255];
34212
34848
  }
34849
+ function relativeLuminance(hex) {
34850
+ const rgb = parseHexColor(hex);
34851
+ if (!rgb) return null;
34852
+ const [r, g, b] = rgb.map((channel) => {
34853
+ const s2 = channel / 255;
34854
+ return s2 <= 0.03928 ? s2 / 12.92 : ((s2 + 0.055) / 1.055) ** 2.4;
34855
+ });
34856
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
34857
+ }
34858
+ function contrastRatio(foreground, background) {
34859
+ const a = relativeLuminance(foreground);
34860
+ const b = relativeLuminance(background);
34861
+ if (a === null || b === null) return 1;
34862
+ const lighter = Math.max(a, b);
34863
+ const darker = Math.min(a, b);
34864
+ return Math.round((lighter + 0.05) / (darker + 0.05) * 10) / 10;
34865
+ }
34213
34866
  function detectSupportsBackground(env3 = process.env, isTTY = process.stdout.isTTY ?? false) {
34214
34867
  if (!isTTY) return false;
34215
34868
  if (typeof env3["NO_COLOR"] === "string" && env3["NO_COLOR"] !== "") return false;
@@ -35647,296 +36300,26 @@ var nightfoxPresets = {
35647
36300
  };
35648
36301
 
35649
36302
  // src/theme-presets/options.ts
35650
- var THEME_OPTIONS = [
35651
- {
35652
- id: "catppuccin",
35653
- name: "Catppuccin Mocha",
35654
- description: "Warm pastel \u2014 the original WrongStack default"
35655
- },
35656
- {
35657
- id: "tokyo-night",
35658
- name: "Tokyo Night",
35659
- description: "Cool blue-purple, calm low-contrast coding palette"
35660
- },
35661
- { id: "nord", name: "Nord", description: "Arctic, muted blues and greens \u2014 easy on the eyes" },
35662
- {
35663
- id: "cyberpunk",
35664
- name: "Cyberpunk",
35665
- description: "Hot pink + neon cyan, high-contrast night mode"
35666
- },
35667
- { id: "dracula", name: "Dracula", description: "Classic purple-on-black, vivid accents" },
35668
- {
35669
- id: "gruvbox-dark",
35670
- name: "Gruvbox Dark",
35671
- description: "Warm earthy retro palette \u2014 orange, olive and muted aqua"
35672
- },
35673
- {
35674
- id: "solarized-dark",
35675
- name: "Solarized Dark",
35676
- description: "Precision contrast, base16 classic with teal undertones"
35677
- },
35678
- {
35679
- id: "one-dark",
35680
- name: "One Dark",
35681
- description: "Atom's iconic dark palette \u2014 blue-led with warm accents"
35682
- },
35683
- {
35684
- id: "monokai",
35685
- name: "Monokai",
35686
- description: "Sublime Text classic \u2014 vivid magenta, cyan and lime"
35687
- },
35688
- {
35689
- id: "rose-pine",
35690
- name: "Ros\xE9 Pine",
35691
- description: "Aesthetic pine & foam \u2014 soft pastel evening tones"
35692
- },
35693
- { id: "kanagawa", name: "Kanagawa", description: "Hokusai-inspired waves \u2014 sumi ink on washi" },
35694
- {
35695
- id: "ayu-dark",
35696
- name: "Ayu Dark",
35697
- description: "Simple, pleasant dark \u2014 warm orange + cool blue mix"
35698
- },
35699
- {
35700
- id: "everforest",
35701
- name: "Everforest",
35702
- description: "Green-based comfort palette \u2014 forest greens and warm tans"
35703
- },
35704
- {
35705
- id: "night-owl",
35706
- name: "Night Owl",
35707
- description: "Sarah Drasner's night theme \u2014 deep navy with bold accents"
35708
- },
35709
- {
35710
- id: "synthwave",
35711
- name: "Synthwave '84",
35712
- description: "Hot pink + neon cyan on deep purple \u2014 80s retro glow"
35713
- },
35714
- {
35715
- id: "github-dark",
35716
- name: "GitHub Dark",
35717
- description: "GitHub's default dark \u2014 crisp blue on near-black"
35718
- },
35719
- {
35720
- id: "material-ocean",
35721
- name: "Material Ocean",
35722
- description: "Deepest Material variant \u2014 ink blue with pastel accents"
35723
- },
35724
- { id: "nightfox", name: "Nightfox", description: "Balanced slate blue with muted sage and rose" },
35725
- {
35726
- id: "oxocarbon",
35727
- name: "Oxocarbon",
35728
- description: "IBM Carbon-derived \u2014 neutral greys, electric blue & pink"
35729
- },
35730
- {
35731
- id: "catppuccin-macchiato",
35732
- name: "Catppuccin Macchiato",
35733
- description: "Warmer, one shade lighter than Mocha"
35734
- },
35735
- {
35736
- id: "catppuccin-frappe",
35737
- name: "Catppuccin Frapp\xE9",
35738
- description: "The lightest Catppuccin dark \u2014 gentle midday contrast"
35739
- },
35740
- {
35741
- id: "gruvbox-material",
35742
- name: "Gruvbox Material",
35743
- description: "Softened Gruvbox \u2014 same warmth, lower eye strain"
35744
- },
35745
- {
35746
- id: "tokyo-night-storm",
35747
- name: "Tokyo Night Storm",
35748
- description: "Tokyo Night on a lifted blue-grey base"
35749
- },
35750
- {
35751
- id: "rose-pine-moon",
35752
- name: "Ros\xE9 Pine Moon",
35753
- description: "Ros\xE9 Pine at dusk \u2014 deeper base, same soft accents"
35754
- },
35755
- {
35756
- id: "zenburn",
35757
- name: "Zenburn",
35758
- description: "The classic low-contrast grey \u2014 desaturated and calm"
35759
- },
35760
- {
35761
- id: "palenight",
35762
- name: "Palenight",
35763
- description: "Material Palenight \u2014 indigo base, candy accents"
35764
- },
35765
- {
35766
- id: "horizon",
35767
- name: "Horizon",
35768
- description: "Warm coral and mint on charcoal \u2014 sunset gradient"
35769
- },
35770
- {
35771
- id: "sonokai",
35772
- name: "Sonokai",
35773
- description: "Monokai Pro descendant \u2014 punchy on warm graphite"
35774
- },
35775
- {
35776
- id: "edge-dark",
35777
- name: "Edge Dark",
35778
- description: "Clean, evenly-weighted palette on desaturated navy"
35779
- },
35780
- {
35781
- id: "moonfly",
35782
- name: "Moonfly",
35783
- description: "Near-black base with high-chroma accents \u2014 maximum contrast"
35784
- },
35785
- {
35786
- id: "melange",
35787
- name: "Melange",
35788
- description: "Warm sepia and clay \u2014 the least blue dark theme here"
35789
- },
35790
- {
35791
- id: "poimandres",
35792
- name: "Poimandres",
35793
- description: "Teal-forward, low-saturation \u2014 mint on deep indigo"
35794
- },
35795
- {
35796
- id: "vitesse-dark",
35797
- name: "Vitesse Dark",
35798
- description: "Anthony Fu's minimal palette \u2014 muted, print-like"
35799
- },
35800
- {
35801
- id: "aura",
35802
- name: "Aura Dark",
35803
- description: "Vivid purple and spring green on near-black violet"
35804
- },
35805
- {
35806
- id: "dark-plus",
35807
- name: "VS Code Dark+",
35808
- description: "Visual Studio Code's default \u2014 familiar blue/orange/teal"
35809
- },
35810
- { id: "monochrome", name: "Monochrome", description: "Pure grayscale \u2014 no hue, only luminance" },
35811
- {
35812
- id: "matrix",
35813
- name: "Matrix Green",
35814
- description: "Phosphor green CRT terminal \u2014 digital rain aesthetic"
35815
- },
35816
- {
35817
- id: "amber",
35818
- name: "Amber CRT",
35819
- description: "Warm phosphor CRT terminal \u2014 glowing vintage amber"
35820
- },
35821
- {
35822
- id: "cyber-noir",
35823
- name: "Cyber Noir",
35824
- description: "Stark white and slate on jet black \u2014 minimalist high contrast"
35825
- },
35826
- {
35827
- id: "cobalt-mono",
35828
- name: "Cobalt Monochrome",
35829
- description: "Luminous cyan on deep abyss blue \u2014 oceanic blueprint"
35830
- },
35831
- {
35832
- id: "blood-moon",
35833
- name: "Blood Moon",
35834
- description: "Crimson & scarlet on obsidian \u2014 brooding dark mode"
35835
- },
35836
- {
35837
- id: "cobalt2",
35838
- name: "Cobalt2",
35839
- description: "Wes Bos' signature theme \u2014 deep navy with golden yellow & cyan"
35840
- },
35841
- {
35842
- id: "shades-of-purple",
35843
- name: "Shades of Purple",
35844
- description: "Ahmad Awais' bold purple palette with neon yellow & magenta"
35845
- },
35846
- {
35847
- id: "flexoki-dark",
35848
- name: "Flexoki Dark",
35849
- description: "Steph Ango's inky warm paper palette \u2014 natural earthy accents"
35850
- },
35851
- {
35852
- id: "laserwave",
35853
- name: "LaserWave",
35854
- description: "80s retrowave \u2014 neon flamingo and turquoise on violet"
35855
- },
35856
- {
35857
- id: "andromeda",
35858
- name: "Andromeda",
35859
- description: "Deep interstellar dark with vibrant neon teal and pink"
35860
- },
35861
- {
35862
- id: "github-dark-dimmed",
35863
- name: "GitHub Dark Dimmed",
35864
- description: "GitHub's softer slate dark theme \u2014 gentle blues and pastels"
35865
- },
35866
- {
35867
- id: "snazzy",
35868
- name: "Hyper Snazzy",
35869
- description: "Sindre Sorhus' elegant saturated terminal palette"
35870
- },
35871
- {
35872
- id: "tokyo-night-moon",
35873
- name: "Tokyo Night Moon",
35874
- description: "Tokyo Night on balanced deep indigo \u2014 vibrant accents"
35875
- },
35876
- {
35877
- id: "gruvbox-dark-hard",
35878
- name: "Gruvbox Dark Hard",
35879
- description: "Maximum contrast Gruvbox on deep pitch charcoal"
35880
- },
35881
- {
35882
- id: "oceanic-next",
35883
- name: "Oceanic Next",
35884
- description: "Teal-and-slate classic \u2014 calm blue, warm coral"
35885
- },
35886
- {
35887
- id: "one-half-dark",
35888
- name: "One Half Dark",
35889
- description: "Atom's One Half \u2014 One Dark with cleaner contrast"
35890
- },
35891
- { id: "ayu-mirage", name: "Ayu Mirage", description: "Dusk-slate Ayu, between Dark and Light" },
35892
- { id: "seti", name: "Seti", description: "Long-running VS Code classic \u2014 charcoal, gold, cyan" },
35893
- { id: "paraiso-dark", name: "Paraiso Dark", description: "Base16 plum \u2014 warm, muted, low-glare" },
35894
- {
35895
- id: "darcula",
35896
- name: "Darcula",
35897
- description: "JetBrains' default dark \u2014 grey-green with amber"
35898
- },
35899
- {
35900
- id: "slack-dark",
35901
- name: "Slack Aubergine",
35902
- description: "Deep aubergine with sky blue and lime"
35903
- },
35904
- {
35905
- id: "vitesse-black",
35906
- name: "Vitesse Black",
35907
- description: "Vitesse on true black \u2014 least glare here"
35908
- },
35909
- {
35910
- id: "atom-dark",
35911
- name: "Atom Dark",
35912
- description: "The original Atom grey \u2014 neutral, low-chroma"
35913
- },
35914
- {
35915
- id: "github-dark-high-contrast",
35916
- name: "GitHub Dark High Contrast",
35917
- description: "Accessible GitHub \u2014 boosted text and borders"
35918
- },
35919
- {
35920
- id: "contrast-max",
35921
- name: "Maximum Contrast",
35922
- description: "Original \u2014 pure black, AAA-targeted text and borders"
35923
- },
35924
- {
35925
- id: "colorblind-safe",
35926
- name: "Colorblind Safe",
35927
- description: "Original \u2014 blue/orange coding, no red-vs-green reliance"
35928
- },
35929
- {
35930
- id: "sandstone",
35931
- name: "Sandstone",
35932
- description: "Original \u2014 warm stone neutrals with sage and clay"
35933
- },
35934
- {
35935
- id: "everforest-hard",
35936
- name: "Everforest Hard",
35937
- description: "Everforest on its deepest base \u2014 more depth, same greens"
35938
- }
35939
- ];
36303
+ import { THEME_PRESET_IDS, THEME_PRESET_META } from "@wrongstack/core/types";
36304
+ var THEME_OPTIONS = THEME_PRESET_IDS.map((id) => ({
36305
+ id,
36306
+ name: THEME_PRESET_META[id].name,
36307
+ description: THEME_PRESET_META[id].description,
36308
+ family: THEME_PRESET_META[id].family
36309
+ })).sort(
36310
+ // Group by family, keeping canonical order WITHIN each family.
36311
+ //
36312
+ // `THEME_PRESET_IDS` is not family-ordered — the Catppuccin variants sit
36313
+ // ~15 rows apart, and the three `misc-N` buckets scatter single palettes
36314
+ // across the whole list. Sorting here (rather than in the picker) is what
36315
+ // makes the grouping usable: the reducer, the Enter handler and the rendered
36316
+ // window all index into THIS array, so reordering it in one place keeps
36317
+ // `selected` meaning the same thing everywhere.
36318
+ //
36319
+ // `Array.prototype.sort` is stable, so sorting only on `family` preserves
36320
+ // canonical order inside each group without a second comparison.
36321
+ (a, b) => a.family.localeCompare(b.family)
36322
+ );
35940
36323
 
35941
36324
  // src/theme-presets/rose-pine.ts
35942
36325
  var rosePinePresets = {
@@ -35953,7 +36336,7 @@ var rosePinePresets = {
35953
36336
  user: "#f6c177",
35954
36337
  assistant: "#c4a7e7",
35955
36338
  tool: "#9ccfd8",
35956
- success: "#31748f",
36339
+ success: "#3e8fb0",
35957
36340
  warn: "#f6c177",
35958
36341
  error: "#eb6f92",
35959
36342
  borderDefault: "#524f67",
@@ -37060,7 +37443,7 @@ var PANEL_IDS = Object.freeze([
37060
37443
  "connections"
37061
37444
  // Ctrl+N
37062
37445
  ]);
37063
- var TOTAL_SETTINGS_FIELD_COUNT = PANEL_POSITION_FIELD_START + PANEL_IDS.length + 4;
37446
+ var TOTAL_SETTINGS_FIELD_COUNT = PANEL_POSITION_FIELD_START + PANEL_IDS.length + 5;
37064
37447
  var DEFAULT_PANEL_POSITIONS = Object.freeze({
37065
37448
  projectPicker: "bottom",
37066
37449
  fleet: "bottom",
@@ -42671,6 +43054,7 @@ import {
42671
43054
  formatQuotaResetIn,
42672
43055
  getAllProviderQuota,
42673
43056
  onProviderQuota,
43057
+ quotaExhaustionInMs,
42674
43058
  quotaResetInMs,
42675
43059
  quotaWindowLabel,
42676
43060
  worstProviderQuotaWindow
@@ -42679,11 +43063,15 @@ function readWorst() {
42679
43063
  const worst = worstProviderQuotaWindow(getAllProviderQuota());
42680
43064
  if (!worst) return void 0;
42681
43065
  const resetIn = formatQuotaResetIn(quotaResetInMs(worst.window));
43066
+ const exhaustsIn = formatQuotaResetIn(
43067
+ quotaExhaustionInMs(worst.snapshot.providerId, worst.snapshot.meterId, worst.window)
43068
+ );
42682
43069
  return {
42683
43070
  providerId: worst.snapshot.providerId,
42684
43071
  windowLabel: quotaWindowLabel(worst.window),
42685
43072
  usedPercent: worst.window.usedPercent,
42686
43073
  ...resetIn !== void 0 ? { resetIn } : {},
43074
+ ...exhaustsIn !== void 0 ? { exhaustsIn } : {},
42687
43075
  reached: worst.snapshot.reachedWindowId === worst.window.id
42688
43076
  };
42689
43077
  }
@@ -43702,6 +44090,7 @@ function buildSafetyWorkEntries(p) {
43702
44090
  isNoColor
43703
44091
  );
43704
44092
  const reset = quota.resetIn ? ` ${glyphs.clock}${quota.resetIn}` : "";
44093
+ const pace = quota.exhaustsIn ? ` \u2192100% ~${quota.exhaustsIn}` : "";
43705
44094
  return entry("quota", "quota", p, [
43706
44095
  /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(Text2, { color, children: [
43707
44096
  icon(STATUSLINE_ICONS.quota, isNoColor),
@@ -43709,7 +44098,8 @@ function buildSafetyWorkEntries(p) {
43709
44098
  " ",
43710
44099
  pct,
43711
44100
  "%",
43712
- reset
44101
+ reset,
44102
+ pace
43713
44103
  ] }),
43714
44104
  /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(Text2, { color, children: [
43715
44105
  icon(STATUSLINE_ICONS.quota, isNoColor),
@@ -44145,6 +44535,11 @@ function buildMinimumChips(p) {
44145
44535
  stateLabel
44146
44536
  ] }) : null,
44147
44537
  yolo && showChip("yolo") ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Text2, { color: chipColor(theme.error, isNoColor), bold: true, children: isNoColor ? "YOLO" : `${STATUSLINE_ICONS.yolo} YOLO` }) : null,
44538
+ // No effort suffix here: minimum mode is the DEFAULT statusline and its
44539
+ // chips are single-level elements the fitter can only DROP, never degrade,
44540
+ // so a qualifier on this chip could never shed — it would evict trailing
44541
+ // chips at moderate widths instead. Same reason the minimum context chip
44542
+ // omits its `[strategy]`, which the detailed full level does render.
44148
44543
  showChip("model") ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(Text2, { color: chipColor(theme.monitor.agents, isNoColor), children: [
44149
44544
  provider ? `${provider}/` : "",
44150
44545
  model
@@ -44372,6 +44767,7 @@ var STATUSLINE_SIDE_INSET = 1;
44372
44767
  var StatusBar = (0, import_react51.memo)(function StatusBar2({
44373
44768
  model,
44374
44769
  provider,
44770
+ effort,
44375
44771
  version,
44376
44772
  latestVersion,
44377
44773
  updateAvailable,
@@ -44510,14 +44906,19 @@ var StatusBar = (0, import_react51.memo)(function StatusBar2({
44510
44906
  stateLabel
44511
44907
  ] }) : null;
44512
44908
  const modelColor = isNoColor ? void 0 : theme.monitor.agents;
44909
+ const effortSuffix = effort ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Text2, { dimColor: !isNoColor, children: ` \xB7 ${effort}` }) : null;
44513
44910
  const modelStatusChip = showChip("model") ? /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(Text2, { color: modelColor, children: [
44514
44911
  provider ? /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(Text2, { dimColor: !isNoColor, children: [
44515
44912
  provider,
44516
44913
  "/"
44517
44914
  ] }) : null,
44518
- model
44915
+ model,
44916
+ effortSuffix
44917
+ ] }) : null;
44918
+ const modelShortChip = showChip("model") ? /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(Text2, { color: modelColor, children: [
44919
+ model,
44920
+ effortSuffix
44519
44921
  ] }) : null;
44520
- const modelShortChip = showChip("model") ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Text2, { color: modelColor, children: model }) : null;
44521
44922
  const modelMicroChip = showChip("model") ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Text2, { color: modelColor, children: truncateChip(model, 10) }) : null;
44522
44923
  const indexStatusChip = buildIndexStatusChip(indexState, showChip, isNoColor);
44523
44924
  const quota = useProviderQuota();
@@ -45907,7 +46308,7 @@ function TodosMonitor({ todos }) {
45907
46308
  setSelected((value) => Math.min(Math.max(0, ordered.length - 1), value + 1));
45908
46309
  }
45909
46310
  });
45910
- const renderRow4 = (t, idx) => {
46311
+ const renderRow5 = (t, idx) => {
45911
46312
  const isSelected = window2.start + idx === selected;
45912
46313
  const marker = isSelected ? "\u203A " : " ";
45913
46314
  const num = String(todos.indexOf(t) + 1).padStart(2);
@@ -46003,9 +46404,9 @@ function TodosMonitor({ todos }) {
46003
46404
  accent: theme.warn
46004
46405
  }
46005
46406
  ) : twoCols ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(Box2, { flexDirection: "row", gap: 3, marginTop: 1, children: [
46006
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Box2, { flexDirection: "column", width: colWidth, children: visible.slice(0, mid).map((t, i) => renderRow4(t, i)) }),
46007
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Box2, { flexDirection: "column", width: colWidth, children: visible.slice(mid).map((t, i) => renderRow4(t, mid + i)) })
46008
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Box2, { flexDirection: "column", marginTop: 1, children: visible.map(renderRow4) })
46407
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Box2, { flexDirection: "column", width: colWidth, children: visible.slice(0, mid).map((t, i) => renderRow5(t, i)) }),
46408
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Box2, { flexDirection: "column", width: colWidth, children: visible.slice(mid).map((t, i) => renderRow5(t, mid + i)) })
46409
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Box2, { flexDirection: "column", marginTop: 1, children: visible.map(renderRow5) })
46009
46410
  ]
46010
46411
  }
46011
46412
  );
@@ -46439,6 +46840,7 @@ function AppStatusRegion({
46439
46840
  {
46440
46841
  provider: liveProvider,
46441
46842
  model: liveModel,
46843
+ effort: getSettings ? state.settingsPicker.open ? state.settingsPicker.reasoningEffort : getSettings().reasoningEffort : void 0,
46442
46844
  version: appVersion,
46443
46845
  latestVersion,
46444
46846
  updateAvailable,
@@ -51072,1249 +51474,678 @@ function collectMultiFileDiffItems(toolName, output, _input) {
51072
51474
  diffText = trimmed;
51073
51475
  }
51074
51476
  if (!diffText?.trim()) return void 0;
51075
- const blocks = splitGitStyleDiff(diffText);
51076
- if (blocks.length === 0) {
51077
- const path13 = explicitFiles[0] ?? extractPatchHeaderPath(diffText);
51078
- return [{ path: path13, diff: diffText }];
51079
- }
51080
- return blocks.map((block, idx) => ({
51081
- path: explicitFiles[idx] ?? block.path,
51082
- diff: block.diff
51083
- }));
51084
- }
51085
- return void 0;
51086
- }
51087
- function extractUnifiedDiffText(text) {
51088
- const lines = text.split("\n");
51089
- const start = lines.findIndex(
51090
- (line) => (
51091
- // Real unified-diff `--- ` headers carry a file path, not a label like
51092
- // `--- SAGE: ...`; reject uppercase-label patterns that hook notices
51093
- // append to tool output after a blank-line separator.
51094
- line.startsWith("--- ") && !/^--- [A-Z]{2,}:/.test(line) || line.startsWith("diff --git") || line.startsWith("@@")
51095
- )
51096
- );
51097
- if (start === -1) return void 0;
51098
- let end = lines.length;
51099
- for (let i = start + 1; i < lines.length; i++) {
51100
- if (lines[i]?.length === 0) {
51101
- end = i;
51102
- break;
51103
- }
51104
- }
51105
- return lines.slice(start, end).join("\n");
51106
- }
51107
- function newFileDiffFromWriteInput(output, input) {
51108
- if (!input || typeof input !== "object") return void 0;
51109
- const obj = input;
51110
- const content = stringOf(obj["content"]);
51111
- if (content === void 0) return void 0;
51112
- const path13 = stringOf(output["path"]) ?? stringOf(obj["path"]) ?? "new file";
51113
- const lines = content === "" ? [] : content.replace(/\n$/, "").split("\n");
51114
- const header = [`+++ ${path13}`, `@@ -0,0 +1,${lines.length} @@`];
51115
- return [...header, ...lines.map((line) => `+${line}`)].join("\n");
51116
- }
51117
- function splitGitStyleDiff(diff2) {
51118
- const lines = diff2.split("\n");
51119
- const blocks = [];
51120
- let current = null;
51121
- const flush = () => {
51122
- if (!current) return;
51123
- const text = current.body.join("\n").trim();
51124
- if (text) blocks.push({ path: current.path, diff: text });
51125
- current = null;
51126
- };
51127
- for (const line of lines) {
51128
- if (line.startsWith("diff --git ")) {
51129
- flush();
51130
- current = { path: parseDiffGitPath(line), body: [line], hasGitHeader: true };
51131
- continue;
51132
- }
51133
- if (current) {
51134
- current.body.push(line);
51135
- if (!current.hasGitHeader && line.startsWith("--- a/") && current.body.length > 2) {
51136
- const carriedBody = current.body.slice(0, -1);
51137
- const carriedPath = current.path;
51138
- const text = carriedBody.join("\n").trim();
51139
- if (text) blocks.push({ path: carriedPath, diff: text });
51140
- current = {
51141
- path: line.slice("--- a/".length).trim(),
51142
- body: [line],
51143
- hasGitHeader: false
51144
- };
51145
- }
51146
- } else if (line.startsWith("--- a/")) {
51147
- current = { path: line.slice("--- a/".length).trim(), body: [line], hasGitHeader: false };
51148
- } else if (line.startsWith("+++ b/")) {
51149
- current = { body: [line], hasGitHeader: false };
51150
- }
51151
- }
51152
- flush();
51153
- if (blocks.length === 1 && !blocks[0].path) {
51154
- const path13 = extractPatchHeaderPath(blocks[0].diff);
51155
- if (path13) blocks[0] = { path: path13, diff: blocks[0].diff };
51156
- }
51157
- return blocks;
51158
- }
51159
- function parseDiffGitPath(line) {
51160
- const rest = line.slice("diff --git ".length).trim();
51161
- if (!rest) return void 0;
51162
- const sep = rest.lastIndexOf(" b/");
51163
- if (sep > 0 && sep > rest.length - sep - 3) {
51164
- return rest.slice(sep + 3);
51165
- }
51166
- const spaceIdx = rest.lastIndexOf(" ");
51167
- if (spaceIdx > 0) {
51168
- const rhs = rest.slice(spaceIdx + 1);
51169
- return rhs.startsWith("b/") ? rhs.slice(2) : rhs;
51170
- }
51171
- return void 0;
51172
- }
51173
- function extractPatchHeaderPath(diffText) {
51174
- for (const line of diffText.split("\n")) {
51175
- if (line.startsWith("+++ ")) {
51176
- const path13 = line.slice(4).trim();
51177
- const cleaned = path13.replace(/^b\//, "").split(" ")[0].trim();
51178
- return cleaned || void 0;
51179
- }
51180
- }
51181
- return void 0;
51182
- }
51183
-
51184
- // src/components/history/diff-model.ts
51185
- var DIFF_MAX_LINES = 200;
51186
- var MULTI_DIFF_MAX_FILES = 20;
51187
- var MULTI_DIFF_MAX_ROWS = 400;
51188
- var MULTI_DIFF_SUMMARY_THRESHOLD = 5;
51189
- function summarizeMultiFileDiffs(items) {
51190
- let added = 0;
51191
- let removed = 0;
51192
- let hiddenAdded = 0;
51193
- let hiddenRemoved = 0;
51194
- let truncatedFiles = 0;
51195
- for (const item of items) {
51196
- added += item.preview.added;
51197
- removed += item.preview.removed;
51198
- hiddenAdded += item.preview.hiddenAdded;
51199
- hiddenRemoved += item.preview.hiddenRemoved;
51200
- if (item.preview.hidden > 0) truncatedFiles += 1;
51201
- }
51202
- const omitted = items.omitted;
51203
- if (omitted) {
51204
- added += omitted.added;
51205
- removed += omitted.removed;
51206
- }
51207
- return {
51208
- fileCount: items.length + (omitted?.fileCount ?? 0),
51209
- added,
51210
- removed,
51211
- hiddenAdded,
51212
- hiddenRemoved,
51213
- truncatedFiles,
51214
- omittedFiles: omitted?.fileCount ?? 0
51215
- };
51216
- }
51217
- function formatMultiDiffSummary(summary, threshold = MULTI_DIFF_SUMMARY_THRESHOLD) {
51218
- if (threshold === 0) return null;
51219
- const effectiveThreshold = threshold < 0 ? MULTI_DIFF_SUMMARY_THRESHOLD : threshold;
51220
- if (summary.fileCount < effectiveThreshold) return null;
51221
- const parts = [`${summary.fileCount} files`];
51222
- if (summary.added > 0) parts.push(`+${summary.added}`);
51223
- if (summary.removed > 0) parts.push(`-${summary.removed}`);
51224
- if (summary.hiddenAdded > 0 || summary.hiddenRemoved > 0) {
51225
- const hiddenParts = [];
51226
- if (summary.hiddenAdded > 0) hiddenParts.push(`+${summary.hiddenAdded}`);
51227
- if (summary.hiddenRemoved > 0) hiddenParts.push(`-${summary.hiddenRemoved}`);
51228
- parts.push(
51229
- `\u2026 ${hiddenParts.join(" ")} hidden across ${summary.truncatedFiles} file${summary.truncatedFiles === 1 ? "" : "s"}`
51230
- );
51231
- }
51232
- const omittedFiles = summary.omittedFiles ?? 0;
51233
- if (omittedFiles > 0) {
51234
- parts.push(`${omittedFiles} more file${omittedFiles === 1 ? "" : "s"} not rendered`);
51235
- }
51236
- return parts.join(" \xB7 ");
51237
- }
51238
- function formatDiffStats(added, removed) {
51239
- const parts = [];
51240
- if (added > 0) parts.push(`added ${added} line${added === 1 ? "" : "s"}`);
51241
- if (removed > 0) parts.push(`removed ${removed} line${removed === 1 ? "" : "s"}`);
51242
- if (parts.length === 0) return null;
51243
- const joined = parts.join(", ");
51244
- return joined.charAt(0).toUpperCase() + joined.slice(1);
51245
- }
51246
- function parseUnifiedDiff(diff2, maxLines) {
51247
- return parseUnifiedDiffPreview(diff2, maxLines);
51248
- }
51249
- function extractDiffPreview(toolName, output, input) {
51250
- if (!output) return void 0;
51251
- const text = output.trim();
51252
- if (!text) return void 0;
51253
- let diff2;
51254
- if (toolName === "edit" || toolName === "diff" || toolName === "write") {
51255
- const parsed = tryParseJson(text);
51256
- if (parsed && typeof parsed === "object") {
51257
- const obj = parsed;
51258
- diff2 = toolName === "write" && obj["created"] === true ? newFileDiffFromWriteInput(obj, input) ?? stringOf(obj["diff"]) : stringOf(obj["diff"]);
51259
- } else {
51260
- diff2 = extractUnifiedDiffText(text);
51261
- }
51262
- } else if (toolName === "patch") {
51263
- const parsed = tryParseJson(text);
51264
- if (parsed && typeof parsed === "object") {
51265
- diff2 = stringOf(parsed["diff"]) ?? stringOf(parsed["stdout"]);
51266
- } else if (text.includes("@@") || text.startsWith("---")) {
51267
- diff2 = text;
51268
- }
51269
- } else if (toolName === "replace") {
51270
- const parsed = tryParseJson(text);
51271
- if (parsed && typeof parsed === "object") {
51272
- diff2 = joinReplaceDiffs(parsed);
51273
- }
51274
- }
51275
- if (!diff2?.trim() || diff2.startsWith("(no-op")) return void 0;
51276
- const preview = parseUnifiedDiff(diff2, DIFF_MAX_LINES);
51277
- return preview.rows.length > 0 ? preview : void 0;
51278
- }
51279
- function extractMultiFileDiffs(toolName, output, input) {
51280
- if (!output) return void 0;
51281
- const items = collectMultiFileDiffItems(toolName, output, input);
51282
- if (items === void 0) return void 0;
51283
- if (items.length === 0) return void 0;
51284
- const previews = [];
51285
- const candidateItems = items.slice(0, MULTI_DIFF_MAX_FILES);
51286
- let remainingRows = MULTI_DIFF_MAX_ROWS;
51287
- let visited = 0;
51288
- for (const item of candidateItems) {
51289
- if (remainingRows <= 0) break;
51290
- visited++;
51291
- const preview = parseUnifiedDiff(item.diff, Math.min(DIFF_MAX_LINES, remainingRows));
51292
- if (preview.rows.length === 0) continue;
51293
- previews.push({ path: item.path ?? "unknown file", preview });
51294
- remainingRows -= preview.rows.length;
51295
- }
51296
- const omittedItems = items.slice(visited);
51297
- if (omittedItems.length > 0) {
51298
- let added = 0;
51299
- let removed = 0;
51300
- for (const item of omittedItems) {
51301
- const summary = countUnifiedDiffChanges(item.diff);
51302
- added += summary.added;
51303
- removed += summary.removed;
51304
- }
51305
- previews.omitted = { fileCount: omittedItems.length, added, removed };
51306
- }
51307
- return previews.length > 0 ? previews : void 0;
51308
- }
51309
-
51310
- // src/components/history/code-block.tsx
51311
- var import_jsx_runtime49 = __toESM(require_jsx_runtime(), 1);
51312
- var MAX_CODE_LINES = 80;
51313
- var DIFF_FALLBACK_WRAP_WIDTH = 100;
51314
- var HARD_TAB_WIDTH = 8;
51315
- function expandTabs(text, tabWidth = HARD_TAB_WIDTH) {
51316
- if (!text.includes(" ")) return text;
51317
- let out = "";
51318
- let col = 0;
51319
- for (const ch of text) {
51320
- if (ch === " ") {
51321
- const advance = tabWidth - col % tabWidth;
51322
- out += " ".repeat(advance);
51323
- col += advance;
51324
- } else {
51325
- out += ch;
51326
- col += 1;
51327
- }
51328
- }
51329
- return out;
51330
- }
51331
- function CodeBlockImpl({
51332
- code,
51333
- lang,
51334
- contentWidth
51335
- }) {
51336
- useActiveTheme();
51337
- let lines = sanitizeTerminalText(code, HARD_TAB_WIDTH).replace(/\n+$/, "").split("\n");
51338
- const hidden = Math.max(0, lines.length - MAX_CODE_LINES);
51339
- if (hidden > 0) lines = lines.slice(0, MAX_CODE_LINES);
51340
- const gutterW = String(lines.length).length;
51341
- const maxW = Math.max(1, Math.min(contentWidth - 4 - gutterW - 1, 120));
51342
- let carry = {};
51343
- const rows = lines.map((raw) => {
51344
- const expanded = expandTabs(raw);
51345
- const display = truncateDisplay(expanded, maxW);
51346
- const r = highlightLine(display, lang, carry);
51347
- carry = r.carry;
51348
- return r.tokens;
51349
- });
51350
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(HistoryRail, { color: theme.borderDefault, children: [
51351
- lang !== "plain" ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: lang }) : null,
51352
- rows.map((tokens, i) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Text2, { children: [
51353
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: `${String(i + 1).padStart(gutterW, " ")} ` }),
51354
- tokens.length === 0 ? " " : tokens.map((t, j) => /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
51355
- Text2,
51356
- {
51357
- dimColor: Boolean(t.dim),
51358
- bold: Boolean(t.bold),
51359
- ...t.color ? { color: t.color } : {},
51360
- children: t.text
51361
- },
51362
- j
51363
- ))
51364
- ] }, i)),
51365
- hidden > 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, italic: true, children: `\u2026 +${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
51366
- ] });
51367
- }
51368
- function DiffFileBlock({
51369
- path: path13,
51370
- preview,
51371
- useColor = true,
51372
- contentWidth
51373
- }) {
51374
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Box2, { flexDirection: "column", marginTop: 1, children: [
51375
- /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Box2, { flexDirection: "row", marginBottom: 0, children: [
51376
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { bold: true, color: theme.accent, children: `\u25B8 ${path13}` }),
51377
- preview.added > 0 || preview.removed > 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: ` (${preview.added > 0 ? `+${preview.added}` : ""}${preview.added > 0 && preview.removed > 0 ? " " : ""}${preview.removed > 0 ? `-${preview.removed}` : ""})` }) : null
51378
- ] }),
51379
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
51380
- DiffBlock,
51381
- {
51382
- rows: preview.rows,
51383
- hidden: preview.hidden,
51384
- added: preview.added,
51385
- removed: preview.removed,
51386
- hiddenAdded: preview.hiddenAdded,
51387
- hiddenRemoved: preview.hiddenRemoved,
51388
- useColor,
51389
- lang: langFromPath(path13),
51390
- showStats: false,
51391
- contentWidth
51392
- }
51393
- )
51394
- ] });
51395
- }
51396
- function applyWashTokens(tokens, onWash) {
51397
- if (!onWash) return tokens;
51398
- return tokens.map((t) => {
51399
- if (t.color !== "syntax.comment") return t;
51400
- return { ...t, color: "syntax.commentOnWash", dim: false };
51401
- });
51402
- }
51403
- function renderTokens(tokens, onWash = false) {
51404
- const effective = applyWashTokens(tokens, onWash);
51405
- if (effective.length === 0) return " ";
51406
- return effective.map((t, j) => /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
51407
- Text2,
51408
- {
51409
- dimColor: Boolean(t.dim),
51410
- bold: Boolean(t.bold),
51411
- ...t.color ? { color: t.color } : {},
51412
- children: t.text
51413
- },
51414
- j
51415
- ));
51416
- }
51417
- function wrapTokens(tokens, width) {
51418
- if (width <= 0) return [tokens];
51419
- const segments = [];
51420
- let current = [];
51421
- let currentWidth = 0;
51422
- for (const t of tokens) {
51423
- let text = t.text;
51424
- while (text.length > 0) {
51425
- const room = width - currentWidth;
51426
- if (room <= 0) {
51427
- segments.push(current);
51428
- current = [];
51429
- currentWidth = 0;
51430
- continue;
51431
- }
51432
- const [piece, rest] = splitDisplay(text, room);
51433
- if (!piece) {
51434
- segments.push(current);
51435
- current = [];
51436
- currentWidth = 0;
51437
- continue;
51438
- }
51439
- current.push({ ...t, text: piece });
51440
- currentWidth += displayWidth(piece);
51441
- text = rest;
51442
- }
51443
- }
51444
- if (current.length > 0 || segments.length === 0) segments.push(current);
51445
- return segments;
51446
- }
51447
- function DiffBlock({
51448
- rows,
51449
- hidden,
51450
- added = 0,
51451
- removed = 0,
51452
- hiddenAdded = 0,
51453
- hiddenRemoved = 0,
51454
- useColor = true,
51455
- lang = "plain",
51456
- showStats = true,
51457
- contentWidth
51458
- }) {
51459
- const lineNoOf = (row) => row.kind === "del" ? row.oldLine : row.newLine ?? row.oldLine;
51460
- let gutterWidth = 1;
51461
- for (const r of rows) {
51462
- const n = lineNoOf(r);
51463
- if (typeof n === "number" && String(n).length > gutterWidth) gutterWidth = String(n).length;
51464
- }
51465
- const blank = " ".repeat(gutterWidth);
51466
- const markerFor = (kind) => {
51467
- if (kind === "add") return "+";
51468
- if (kind === "del") return "-";
51469
- return " ";
51470
- };
51471
- const textForDisplay = (row) => {
51472
- if ((row.kind === "add" || row.kind === "del" || row.kind === "ctx") && row.text.length > 0) {
51473
- return sanitizeTerminalText(expandTabs(row.text.slice(1)), HARD_TAB_WIDTH) || " ";
51474
- }
51475
- return sanitizeTerminalText(expandTabs(row.text), HARD_TAB_WIDTH) || " ";
51476
- };
51477
- const stats = showStats ? formatDiffStats(added, removed) : null;
51478
- const terminalWrapGuard = typeof contentWidth === "number" ? 1 : 0;
51479
- const bodyBudget = typeof contentWidth === "number" ? Math.max(1, contentWidth - (2 + 3 + gutterWidth + 3 + terminalWrapGuard)) : DIFF_FALLBACK_WRAP_WIDTH;
51480
- const hasWidth = typeof contentWidth === "number";
51481
- const padBody = (seg) => {
51482
- if (!hasWidth) return 0;
51483
- const len = seg.reduce((n, t) => n + displayWidth(t.text), 0);
51484
- return Math.max(0, bodyBudget - len);
51485
- };
51486
- const contPrefix = ` ${blank} `;
51487
- const hiddenStats = [];
51488
- if (hiddenAdded > 0) hiddenStats.push(`+${hiddenAdded}`);
51489
- if (hiddenRemoved > 0) hiddenStats.push(`-${hiddenRemoved}`);
51490
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Box2, { flexDirection: "column", marginLeft: 2, marginTop: 0, children: [
51491
- stats ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Text2, { children: [
51492
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: "\u23BF " }),
51493
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { children: stats })
51494
- ] }) : null,
51495
- rows.map((row, i) => {
51496
- const key2 = i;
51497
- if (row.kind === "hunk") {
51498
- if (i === 0) return null;
51499
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: ` ${blank} \u22EF` }, key2);
51500
- }
51501
- if (row.kind === "meta") {
51502
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: ` ${blank} ${sanitizeTerminalText(row.text)}` }, key2);
51503
- }
51504
- const n = lineNoOf(row);
51505
- const ln = typeof n === "number" ? String(n).padStart(gutterWidth, " ") : blank;
51506
- const body = textForDisplay(row);
51507
- const tokens = highlightLine(body, lang).tokens;
51508
- const segments = wrapTokens(tokens, bodyBudget);
51509
- if (row.kind === "ctx") {
51510
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Box2, { flexDirection: "column", children: segments.map((seg, si) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Text2, { children: [
51511
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: si === 0 ? ` ${ln} ` : contPrefix }),
51512
- renderTokens(seg)
51513
- ] }, si)) }, key2);
51514
- }
51515
- const marker = markerFor(row.kind);
51516
- const markerColor = row.kind === "add" ? theme.success : theme.error;
51517
- if (useColor) {
51518
- const bg = row.kind === "add" ? theme.diffAddBg : theme.diffDelBg;
51519
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Box2, { flexDirection: "column", children: segments.map((seg, si) => {
51520
- const pad = padBody(seg);
51521
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Text2, { backgroundColor: bg, children: [
51522
- si === 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_jsx_runtime49.Fragment, { children: [
51523
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: ` ${ln} ` }),
51524
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { color: markerColor, bold: true, children: marker }),
51525
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { children: " " })
51526
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: contPrefix }),
51527
- renderTokens(seg, true),
51528
- pad > 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { children: " ".repeat(pad) }) : null
51529
- ] }, si);
51530
- }) }, key2);
51531
- }
51532
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Box2, { flexDirection: "column", children: segments.map((seg, si) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Text2, { children: [
51533
- si === 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_jsx_runtime49.Fragment, { children: [
51534
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: ` ${ln} ` }),
51535
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { color: markerColor, bold: true, children: marker }),
51536
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { children: " " })
51537
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: contPrefix }),
51538
- renderTokens(seg)
51539
- ] }, si)) }, key2);
51540
- }),
51541
- hidden > 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, italic: true, children: ` ${blank} \u2026 ${hidden} more line${hidden === 1 ? "" : "s"}${hiddenStats.length > 0 ? ` (${hiddenStats.join(" ")} hidden)` : ""}` }) : null
51542
- ] });
51543
- }
51544
- var CodeBlock = (0, import_react62.memo)(CodeBlockImpl);
51545
-
51546
- // src/components/confirm-prompt.tsx
51547
- var import_jsx_runtime50 = __toESM(require_jsx_runtime(), 1);
51548
- var BUTTON_COLOR = {
51549
- yes: "green",
51550
- no: "red",
51551
- always: "cyan",
51552
- "always-exact": "cyan",
51553
- "always-command": "cyan",
51554
- "always-tool": "yellow",
51555
- deny: "red"
51556
- };
51557
- function buttonLabels(_suggestedPattern, toolName = "exec") {
51558
- return [
51559
- { decision: "yes", bracket: "[y]", rest: "es " },
51560
- { decision: "no", bracket: "[n]", rest: "o " },
51561
- { decision: "always-exact", bracket: "[a]", rest: " same args " },
51562
- ...toolName === "exec" ? [{ decision: "always-command", bracket: "[c]", rest: " command " }] : [],
51563
- { decision: "always-tool", bracket: "[t]", rest: " tool " },
51564
- { decision: "deny", bracket: "[d]", rest: "eny" }
51565
- ];
51566
- }
51567
- function stringifyInput(input) {
51568
- if (!input || typeof input !== "object") return "";
51569
- const obj = input;
51570
- return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string" && k !== "diff").map(([k, v]) => `${k}: ${truncate(JSON.stringify(v), 80)}`).join(" ");
51477
+ const blocks = splitGitStyleDiff(diffText);
51478
+ if (blocks.length === 0) {
51479
+ const path13 = explicitFiles[0] ?? extractPatchHeaderPath(diffText);
51480
+ return [{ path: path13, diff: diffText }];
51481
+ }
51482
+ return blocks.map((block, idx) => ({
51483
+ path: explicitFiles[idx] ?? block.path,
51484
+ diff: block.diff
51485
+ }));
51486
+ }
51487
+ return void 0;
51571
51488
  }
51572
- function truncate(s2, max) {
51573
- return s2.length <= max ? s2 : `${s2.slice(0, max - 1)}\u2026`;
51489
+ function extractUnifiedDiffText(text) {
51490
+ const lines = text.split("\n");
51491
+ const start = lines.findIndex(
51492
+ (line) => (
51493
+ // Real unified-diff `--- ` headers carry a file path, not a label like
51494
+ // `--- SAGE: ...`; reject uppercase-label patterns that hook notices
51495
+ // append to tool output after a blank-line separator.
51496
+ line.startsWith("--- ") && !/^--- [A-Z]{2,}:/.test(line) || line.startsWith("diff --git") || line.startsWith("@@")
51497
+ )
51498
+ );
51499
+ if (start === -1) return void 0;
51500
+ let end = lines.length;
51501
+ for (let i = start + 1; i < lines.length; i++) {
51502
+ if (lines[i]?.length === 0) {
51503
+ end = i;
51504
+ break;
51505
+ }
51506
+ }
51507
+ return lines.slice(start, end).join("\n");
51574
51508
  }
51575
- function payloadDiff(input) {
51576
- if (!input || typeof input !== "object") return "";
51509
+ function newFileDiffFromWriteInput(output, input) {
51510
+ if (!input || typeof input !== "object") return void 0;
51577
51511
  const obj = input;
51578
- const filePath = typeof obj["path"] === "string" ? obj["path"] : "file";
51579
- if (typeof obj["diff"] === "string" && obj["diff"]) return obj["diff"];
51580
- const newString = obj["new_string"];
51581
- if (typeof newString === "string") {
51582
- const oldString = typeof obj["old_string"] === "string" ? obj["old_string"] : "";
51583
- return unifiedDiff(oldString, newString, { fromFile: filePath, toFile: filePath, context: 2 });
51512
+ const content = stringOf(obj["content"]);
51513
+ if (content === void 0) return void 0;
51514
+ const path13 = stringOf(output["path"]) ?? stringOf(obj["path"]) ?? "new file";
51515
+ const lines = content === "" ? [] : content.replace(/\n$/, "").split("\n");
51516
+ const header = [`+++ ${path13}`, `@@ -0,0 +1,${lines.length} @@`];
51517
+ return [...header, ...lines.map((line) => `+${line}`)].join("\n");
51518
+ }
51519
+ function splitGitStyleDiff(diff2) {
51520
+ const lines = diff2.split("\n");
51521
+ const blocks = [];
51522
+ let current = null;
51523
+ const flush = () => {
51524
+ if (!current) return;
51525
+ const text = current.body.join("\n").trim();
51526
+ if (text) blocks.push({ path: current.path, diff: text });
51527
+ current = null;
51528
+ };
51529
+ for (const line of lines) {
51530
+ if (line.startsWith("diff --git ")) {
51531
+ flush();
51532
+ current = { path: parseDiffGitPath(line), body: [line], hasGitHeader: true };
51533
+ continue;
51534
+ }
51535
+ if (current) {
51536
+ current.body.push(line);
51537
+ if (!current.hasGitHeader && line.startsWith("--- a/") && current.body.length > 2) {
51538
+ const carriedBody = current.body.slice(0, -1);
51539
+ const carriedPath = current.path;
51540
+ const text = carriedBody.join("\n").trim();
51541
+ if (text) blocks.push({ path: carriedPath, diff: text });
51542
+ current = {
51543
+ path: line.slice("--- a/".length).trim(),
51544
+ body: [line],
51545
+ hasGitHeader: false
51546
+ };
51547
+ }
51548
+ } else if (line.startsWith("--- a/")) {
51549
+ current = { path: line.slice("--- a/".length).trim(), body: [line], hasGitHeader: false };
51550
+ } else if (line.startsWith("+++ b/")) {
51551
+ current = { body: [line], hasGitHeader: false };
51552
+ }
51584
51553
  }
51585
- const content = obj["content"];
51586
- if (typeof content === "string" && content) {
51587
- return unifiedDiff("", content, { fromFile: "/dev/null", toFile: filePath, context: 0 });
51554
+ flush();
51555
+ if (blocks.length === 1 && !blocks[0].path) {
51556
+ const path13 = extractPatchHeaderPath(blocks[0].diff);
51557
+ if (path13) blocks[0] = { path: path13, diff: blocks[0].diff };
51588
51558
  }
51589
- return "";
51590
- }
51591
- var CONFIRM_DIFF_MAX_LINES = 20;
51592
- function renderDiffLine(line) {
51593
- const prefix = line.startsWith("+") ? "green" : line.startsWith("-") ? "red" : line.startsWith("@@") ? "cyan" : void 0;
51594
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(Text2, { ...prefix ? { color: prefix } : {}, children: [
51595
- line,
51596
- "\n"
51597
- ] }, line);
51559
+ return blocks;
51598
51560
  }
51599
- function renderDiff(diff2, path13) {
51600
- const preview = parseUnifiedDiff(diff2, CONFIRM_DIFF_MAX_LINES);
51601
- if (preview.rows.length > 0) {
51602
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
51603
- DiffBlock,
51604
- {
51605
- rows: preview.rows,
51606
- hidden: preview.hidden,
51607
- added: preview.added,
51608
- removed: preview.removed,
51609
- hiddenAdded: preview.hiddenAdded,
51610
- hiddenRemoved: preview.hiddenRemoved,
51611
- useColor: theme.supportsBackground,
51612
- lang: langFromPath(path13 ?? "")
51613
- }
51614
- );
51561
+ function parseDiffGitPath(line) {
51562
+ const rest = line.slice("diff --git ".length).trim();
51563
+ if (!rest) return void 0;
51564
+ const sep = rest.lastIndexOf(" b/");
51565
+ if (sep > 0 && sep > rest.length - sep - 3) {
51566
+ return rest.slice(sep + 3);
51615
51567
  }
51616
- const lines = diff2.split("\n").filter((l) => l.length > 0).slice(0, CONFIRM_DIFF_MAX_LINES);
51617
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Box2, { flexDirection: "column", paddingX: 2, children: lines.map((l) => renderDiffLine(l)) });
51568
+ const spaceIdx = rest.lastIndexOf(" ");
51569
+ if (spaceIdx > 0) {
51570
+ const rhs = rest.slice(spaceIdx + 1);
51571
+ return rhs.startsWith("b/") ? rhs.slice(2) : rhs;
51572
+ }
51573
+ return void 0;
51618
51574
  }
51619
- function ConfirmPrompt({
51620
- toolName,
51621
- input,
51622
- suggestedPattern,
51623
- onDecision,
51624
- onEnableYolo,
51625
- destructive,
51626
- boundaryReason,
51627
- writeTargets,
51628
- maxRows
51629
- }) {
51630
- const size = useTerminalSize();
51631
- const rowBudget = maxRows ?? size.rows;
51632
- const buttons = import_react63.default.useRef(/* @__PURE__ */ new Map());
51633
- const partialMouse = import_react63.default.useRef("");
51634
- const decided = import_react63.default.useRef(false);
51635
- const decide = (decision) => {
51636
- if (decided.current) return;
51637
- decided.current = true;
51638
- onDecision(decision);
51639
- };
51640
- const { stdin } = use_stdin_default();
51641
- import_react63.default.useEffect(() => {
51642
- if (!stdin) return;
51643
- const onData = (data) => {
51644
- const chunk = splitTrailingMousePartial(partialMouse.current + data.toString());
51645
- partialMouse.current = chunk.pending;
51646
- for (const event of parseMouseEvents(chunk.consumed)) {
51647
- if (event.kind !== "press" || event.button !== "left") continue;
51648
- for (const [decision, node] of buttons.current) {
51649
- const rect = measure_element_default(node);
51650
- if (event.x > rect.x && event.x <= rect.x + rect.width && event.y > rect.y && event.y <= rect.y + rect.height) {
51651
- decide(decision);
51652
- break;
51653
- }
51654
- }
51655
- }
51656
- };
51657
- stdin.on("data", onData);
51658
- return () => {
51659
- stdin.off("data", onData);
51660
- };
51661
- }, [stdin, decide]);
51662
- import_react63.default.useEffect(() => {
51663
- writeOut("\x07");
51664
- }, []);
51665
- use_input_default((input2, key2) => {
51666
- if (key2.ctrl || key2.meta || decided.current) return;
51667
- if (!input2 || input2 === "\r" || input2 === "\n") return;
51668
- if (input2 === "Y" && !boundaryReason) {
51669
- onEnableYolo();
51670
- return;
51671
- }
51672
- const ch = input2.toLowerCase();
51673
- if (ch === "y") {
51674
- decide("yes");
51675
- } else if (ch === "n") {
51676
- decide("no");
51677
- } else if (ch === "a") {
51678
- decide("always-exact");
51679
- } else if (ch === "c" && toolName === "exec") {
51680
- decide("always-command");
51681
- } else if (ch === "t") {
51682
- decide("always-tool");
51683
- } else if (ch === "d") {
51684
- decide("deny");
51685
- }
51686
- });
51687
- const inputSummary = stringifyInput(input);
51688
- const inp = input;
51689
- const diff2 = payloadDiff(input);
51690
- const diffPath = typeof inp?.path === "string" ? inp.path : void 0;
51691
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(PanelInputProvider, { value: true, children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(MonitorViewportProvider, { value: { columns: size.columns, rows: rowBudget }, children: /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
51692
- MonitorShell,
51693
- {
51694
- accent: "yellow",
51695
- icon: "\u26A0",
51696
- title: `APPROVAL REQUIRED \xB7 ${toolName}`,
51697
- footer: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Box2, { flexDirection: "row", flexWrap: "wrap", children: buttonLabels(suggestedPattern, toolName).map((label) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
51698
- Box2,
51699
- {
51700
- flexShrink: 0,
51701
- ref: (node) => {
51702
- if (node) buttons.current.set(label.decision, node);
51703
- else buttons.current.delete(label.decision);
51704
- },
51705
- children: /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(Text2, { children: [
51706
- /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Text2, { bold: true, color: BUTTON_COLOR[label.decision], children: label.bracket }),
51707
- /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Text2, { dimColor: true, children: label.rest })
51708
- ] })
51709
- },
51710
- label.decision
51711
- )) }),
51712
- children: [
51713
- inputSummary ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Text2, { dimColor: true, children: inputSummary }) : null,
51714
- boundaryReason ? /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(Text2, { color: "yellow", children: [
51715
- "KANBAN BOUNDARY: ",
51716
- boundaryReason
51717
- ] }) : null,
51718
- writeTargets && writeTargets.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(Text2, { color: "yellow", children: [
51719
- "WRITES: ",
51720
- writeTargets.slice(0, 5).join(", "),
51721
- writeTargets.length > 5 ? ` (+${writeTargets.length - 5} more)` : ""
51722
- ] }) : null,
51723
- diff2 ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Box2, { flexDirection: "column", marginY: 1, children: renderDiff(diff2, diffPath) }) : null,
51724
- /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(Text2, { dimColor: true, children: [
51725
- "Remember: [a] this input; ",
51726
- toolName === "exec" ? "[c] executable, any args; " : "",
51727
- "[t] tool, any input. Destructive calls still prompt."
51728
- ] }),
51729
- !boundaryReason ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Box2, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(Text2, { dimColor: true, children: [
51730
- " ",
51731
- "Tip: press",
51732
- " ",
51733
- /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Text2, { bold: true, color: "yellow", children: "Y" }),
51734
- " ",
51735
- "to enable YOLO mode",
51736
- destructive ? " (this destructive approval remains)" : " (skips future routine approvals)",
51737
- "."
51738
- ] }) }) : null
51739
- ]
51575
+ function extractPatchHeaderPath(diffText) {
51576
+ for (const line of diffText.split("\n")) {
51577
+ if (line.startsWith("+++ ")) {
51578
+ const path13 = line.slice(4).trim();
51579
+ const cleaned = path13.replace(/^b\//, "").split(" ")[0].trim();
51580
+ return cleaned || void 0;
51740
51581
  }
51741
- ) }) });
51582
+ }
51583
+ return void 0;
51742
51584
  }
51743
51585
 
51744
- // src/components/connections-panel.tsx
51745
- var import_react64 = __toESM(require_react(), 1);
51746
-
51747
- // src/connection-actions.ts
51748
- import * as net from "node:net";
51749
- import {
51750
- ChronicleProjectServerClient,
51751
- createChronicleProjectAccess,
51752
- resolveChronicleProjectServerOptions
51753
- } from "@wrongstack/core/chronicle";
51754
- import {
51755
- isMailboxProjectServerAvailable,
51756
- MailboxProjectServerConnection
51757
- } from "@wrongstack/core/coordination";
51758
- import { SessionCatalogProjectClient } from "@wrongstack/core/session-catalog";
51759
- import { resolveWstackPaths as resolveWstackPaths2, toErrorMessage } from "@wrongstack/core/utils";
51760
- import {
51761
- closeKanbanServerConnections,
51762
- getKanbanServerConnection,
51763
- isKanbanServerAvailable
51764
- } from "@wrongstack/kanban";
51765
- import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
51766
- import {
51767
- checkCodebaseIndexServerHealth,
51768
- ensureCodebaseIndexServer,
51769
- shutdownCodebaseIndexServer
51770
- } from "@wrongstack/tools";
51771
- var RESTARTABLE_SERVICES = [
51772
- "session-catalog",
51773
- "chronicle",
51774
- "codebase-index",
51775
- "sage",
51776
- "kanban",
51777
- "mailbox"
51778
- ];
51779
- function isRestartableService(serviceId) {
51780
- return RESTARTABLE_SERVICES.includes(serviceId);
51781
- }
51782
- function isEndpointAlive(endpoint) {
51783
- return new Promise((resolve) => {
51784
- const sock = net.createConnection(endpoint);
51785
- const timer = setTimeout(() => {
51786
- sock.destroy();
51787
- resolve(false);
51788
- }, 500);
51789
- timer.unref?.();
51790
- sock.once("connect", () => {
51791
- clearTimeout(timer);
51792
- sock.destroy();
51793
- resolve(true);
51794
- });
51795
- sock.once("error", () => {
51796
- clearTimeout(timer);
51797
- sock.destroy();
51798
- resolve(false);
51799
- });
51800
- });
51801
- }
51802
- async function waitForShutdown(probe) {
51803
- const pollIntervalMs = 250;
51804
- if (!probe) {
51805
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
51806
- return;
51586
+ // src/components/history/diff-model.ts
51587
+ var DIFF_MAX_LINES = 200;
51588
+ var MULTI_DIFF_MAX_FILES = 20;
51589
+ var MULTI_DIFF_MAX_ROWS = 400;
51590
+ var MULTI_DIFF_SUMMARY_THRESHOLD = 5;
51591
+ function summarizeMultiFileDiffs(items) {
51592
+ let added = 0;
51593
+ let removed = 0;
51594
+ let hiddenAdded = 0;
51595
+ let hiddenRemoved = 0;
51596
+ let truncatedFiles = 0;
51597
+ for (const item of items) {
51598
+ added += item.preview.added;
51599
+ removed += item.preview.removed;
51600
+ hiddenAdded += item.preview.hiddenAdded;
51601
+ hiddenRemoved += item.preview.hiddenRemoved;
51602
+ if (item.preview.hidden > 0) truncatedFiles += 1;
51807
51603
  }
51808
- const deadline = Date.now() + 3e3;
51809
- while (Date.now() < deadline) {
51810
- try {
51811
- const stillUp = await probe();
51812
- if (!stillUp) return;
51813
- } catch {
51814
- return;
51815
- }
51816
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
51604
+ const omitted = items.omitted;
51605
+ if (omitted) {
51606
+ added += omitted.added;
51607
+ removed += omitted.removed;
51817
51608
  }
51609
+ return {
51610
+ fileCount: items.length + (omitted?.fileCount ?? 0),
51611
+ added,
51612
+ removed,
51613
+ hiddenAdded,
51614
+ hiddenRemoved,
51615
+ truncatedFiles,
51616
+ omittedFiles: omitted?.fileCount ?? 0
51617
+ };
51818
51618
  }
51819
- async function executeConnectionAction(serviceId, action, projectRoot, indexDir) {
51820
- switch (serviceId) {
51821
- case "session-catalog":
51822
- return killSessionCatalogServer(projectRoot, action);
51823
- case "kanban":
51824
- return killKanbanServer(projectRoot, action);
51825
- case "sage":
51826
- return killSageServer(projectRoot, action);
51827
- case "chronicle":
51828
- return killChronicleServer(projectRoot, action);
51829
- case "codebase-index":
51830
- return killCodebaseIndexServer(projectRoot, indexDir, action);
51831
- case "mailbox":
51832
- return killMailboxServer(projectRoot, action);
51833
- case "governance":
51834
- return {
51835
- serviceId: "governance",
51836
- action,
51837
- success: false,
51838
- message: "Governance control plane is read-only; daemon restart is not supported."
51839
- };
51840
- default:
51841
- return {
51842
- serviceId,
51843
- action,
51844
- success: false,
51845
- message: `Unknown service: ${serviceId}`
51846
- };
51619
+ function formatMultiDiffSummary(summary, threshold = MULTI_DIFF_SUMMARY_THRESHOLD) {
51620
+ if (threshold === 0) return null;
51621
+ const effectiveThreshold = threshold < 0 ? MULTI_DIFF_SUMMARY_THRESHOLD : threshold;
51622
+ if (summary.fileCount < effectiveThreshold) return null;
51623
+ const parts = [`${summary.fileCount} files`];
51624
+ if (summary.added > 0) parts.push(`+${summary.added}`);
51625
+ if (summary.removed > 0) parts.push(`-${summary.removed}`);
51626
+ if (summary.hiddenAdded > 0 || summary.hiddenRemoved > 0) {
51627
+ const hiddenParts = [];
51628
+ if (summary.hiddenAdded > 0) hiddenParts.push(`+${summary.hiddenAdded}`);
51629
+ if (summary.hiddenRemoved > 0) hiddenParts.push(`-${summary.hiddenRemoved}`);
51630
+ parts.push(
51631
+ `\u2026 ${hiddenParts.join(" ")} hidden across ${summary.truncatedFiles} file${summary.truncatedFiles === 1 ? "" : "s"}`
51632
+ );
51847
51633
  }
51848
- }
51849
- async function killSessionCatalogServer(projectRoot, action) {
51850
- const paths = resolveWstackPaths2({ projectRoot });
51851
- const client = new SessionCatalogProjectClient({
51852
- projectDir: paths.projectDir,
51853
- projectRoot
51854
- });
51855
- try {
51856
- const previousPid = action === "restart" ? await client.callExisting("ping", {}, { timeoutMs: 1e3 }).then(
51857
- (health2) => health2.pid,
51858
- () => void 0
51859
- ) : void 0;
51860
- const result = await client.shutdown(`TUI request: ${action}`);
51861
- if (!result.stopped && action === "shutdown") {
51862
- return {
51863
- serviceId: "session-catalog",
51864
- action,
51865
- success: false,
51866
- message: "Session Catalog IPC daemon is not running"
51867
- };
51868
- }
51869
- if (action === "restart") return await restartSessionCatalogServer(projectRoot, previousPid);
51870
- return {
51871
- serviceId: "session-catalog",
51872
- action,
51873
- success: true,
51874
- message: "Session Catalog IPC daemon shutdown requested"
51875
- };
51876
- } catch (error) {
51877
- return {
51878
- serviceId: "session-catalog",
51879
- action,
51880
- success: false,
51881
- message: toErrorMessage(error)
51882
- };
51883
- } finally {
51884
- await client.close().catch(() => void 0);
51634
+ const omittedFiles = summary.omittedFiles ?? 0;
51635
+ if (omittedFiles > 0) {
51636
+ parts.push(`${omittedFiles} more file${omittedFiles === 1 ? "" : "s"} not rendered`);
51885
51637
  }
51638
+ return parts.join(" \xB7 ");
51886
51639
  }
51887
- async function restartSessionCatalogServer(projectRoot, previousPid) {
51888
- const paths = resolveWstackPaths2({ projectRoot });
51889
- const probe = () => {
51890
- const client = new SessionCatalogProjectClient({
51891
- projectDir: paths.projectDir,
51892
- projectRoot
51893
- });
51894
- return client.callExisting("ping", {}, { timeoutMs: 1e3 }).then(
51895
- () => true,
51896
- () => false
51897
- ).finally(() => client.close().catch(() => void 0));
51898
- };
51899
- await waitForShutdown(probe);
51900
- const verify = new SessionCatalogProjectClient({
51901
- projectDir: paths.projectDir,
51902
- projectRoot
51903
- });
51904
- try {
51905
- const health2 = await verify.ping();
51906
- if (previousPid !== void 0 && health2.pid === previousPid) {
51907
- return {
51908
- serviceId: "session-catalog",
51909
- action: "restart",
51910
- success: false,
51911
- message: `Session Catalog IPC daemon did not restart (owner PID is still ${previousPid})`
51912
- };
51640
+ function formatDiffStats(added, removed) {
51641
+ const parts = [];
51642
+ if (added > 0) parts.push(`added ${added} line${added === 1 ? "" : "s"}`);
51643
+ if (removed > 0) parts.push(`removed ${removed} line${removed === 1 ? "" : "s"}`);
51644
+ if (parts.length === 0) return null;
51645
+ const joined = parts.join(", ");
51646
+ return joined.charAt(0).toUpperCase() + joined.slice(1);
51647
+ }
51648
+ function parseUnifiedDiff(diff2, maxLines) {
51649
+ return parseUnifiedDiffPreview(diff2, maxLines);
51650
+ }
51651
+ function extractDiffPreview(toolName, output, input) {
51652
+ if (!output) return void 0;
51653
+ const text = output.trim();
51654
+ if (!text) return void 0;
51655
+ let diff2;
51656
+ if (toolName === "edit" || toolName === "diff" || toolName === "write") {
51657
+ const parsed = tryParseJson(text);
51658
+ if (parsed && typeof parsed === "object") {
51659
+ const obj = parsed;
51660
+ diff2 = toolName === "write" && obj["created"] === true ? newFileDiffFromWriteInput(obj, input) ?? stringOf(obj["diff"]) : stringOf(obj["diff"]);
51661
+ } else {
51662
+ diff2 = extractUnifiedDiffText(text);
51663
+ }
51664
+ } else if (toolName === "patch") {
51665
+ const parsed = tryParseJson(text);
51666
+ if (parsed && typeof parsed === "object") {
51667
+ diff2 = stringOf(parsed["diff"]) ?? stringOf(parsed["stdout"]);
51668
+ } else if (text.includes("@@") || text.startsWith("---")) {
51669
+ diff2 = text;
51670
+ }
51671
+ } else if (toolName === "replace") {
51672
+ const parsed = tryParseJson(text);
51673
+ if (parsed && typeof parsed === "object") {
51674
+ diff2 = joinReplaceDiffs(parsed);
51913
51675
  }
51914
- return {
51915
- serviceId: "session-catalog",
51916
- action: "restart",
51917
- success: true,
51918
- message: "Session Catalog IPC daemon restarted successfully"
51919
- };
51920
- } catch (error) {
51921
- return {
51922
- serviceId: "session-catalog",
51923
- action: "restart",
51924
- success: false,
51925
- message: `Session Catalog IPC daemon restarted but verification failed: ${toErrorMessage(error)}`
51926
- };
51927
- } finally {
51928
- await verify.close().catch(() => void 0);
51929
51676
  }
51677
+ if (!diff2?.trim() || diff2.startsWith("(no-op")) return void 0;
51678
+ const preview = parseUnifiedDiff(diff2, DIFF_MAX_LINES);
51679
+ return preview.rows.length > 0 ? preview : void 0;
51930
51680
  }
51931
- async function killKanbanServer(projectRoot, action) {
51932
- if (process.env["WRONGSTACK_KANBAN_SERVER"] === "0") {
51933
- return {
51934
- serviceId: "kanban",
51935
- action,
51936
- success: false,
51937
- message: "Kanban IPC daemon is disabled via WRONGSTACK_KANBAN_SERVER=0"
51938
- };
51939
- }
51940
- let connection;
51941
- try {
51942
- connection = await getKanbanServerConnection(projectRoot);
51943
- } catch (error) {
51944
- return {
51945
- serviceId: "kanban",
51946
- action,
51947
- success: false,
51948
- message: toErrorMessage(error)
51949
- };
51950
- }
51951
- if (!connection) {
51952
- return {
51953
- serviceId: "kanban",
51954
- action,
51955
- success: false,
51956
- message: "Kanban IPC daemon is not running"
51957
- };
51681
+ function extractMultiFileDiffs(toolName, output, input) {
51682
+ if (!output) return void 0;
51683
+ const items = collectMultiFileDiffItems(toolName, output, input);
51684
+ if (items === void 0) return void 0;
51685
+ if (items.length === 0) return void 0;
51686
+ const previews = [];
51687
+ const candidateItems = items.slice(0, MULTI_DIFF_MAX_FILES);
51688
+ let remainingRows = MULTI_DIFF_MAX_ROWS;
51689
+ let visited = 0;
51690
+ for (const item of candidateItems) {
51691
+ if (remainingRows <= 0) break;
51692
+ visited++;
51693
+ const preview = parseUnifiedDiff(item.diff, Math.min(DIFF_MAX_LINES, remainingRows));
51694
+ if (preview.rows.length === 0) continue;
51695
+ previews.push({ path: item.path ?? "unknown file", preview });
51696
+ remainingRows -= preview.rows.length;
51958
51697
  }
51959
- try {
51960
- const result = await connection.request("shutdown", {
51961
- reason: `TUI request: ${action}`
51962
- });
51963
- if (!result.stopping) {
51964
- return {
51965
- serviceId: "kanban",
51966
- action,
51967
- success: false,
51968
- message: "Kanban IPC daemon shutdown failed (not confirmed)"
51969
- };
51970
- }
51971
- if (action === "restart") {
51972
- closeKanbanServerConnections();
51973
- return await restartKanbanServer(projectRoot);
51698
+ const omittedItems = items.slice(visited);
51699
+ if (omittedItems.length > 0) {
51700
+ let added = 0;
51701
+ let removed = 0;
51702
+ for (const item of omittedItems) {
51703
+ const summary = countUnifiedDiffChanges(item.diff);
51704
+ added += summary.added;
51705
+ removed += summary.removed;
51974
51706
  }
51975
- return {
51976
- serviceId: "kanban",
51977
- action,
51978
- success: true,
51979
- message: "Kanban IPC daemon shutdown requested"
51980
- };
51981
- } catch (error) {
51982
- return {
51983
- serviceId: "kanban",
51984
- action,
51985
- success: false,
51986
- message: toErrorMessage(error)
51987
- };
51707
+ previews.omitted = { fileCount: omittedItems.length, added, removed };
51988
51708
  }
51709
+ return previews.length > 0 ? previews : void 0;
51989
51710
  }
51990
- async function restartKanbanServer(projectRoot) {
51991
- await waitForShutdown(() => isKanbanServerAvailable(projectRoot));
51992
- try {
51993
- const connection = await getKanbanServerConnection(projectRoot);
51994
- if (!connection) {
51995
- return {
51996
- serviceId: "kanban",
51997
- action: "restart",
51998
- success: false,
51999
- message: "Kanban IPC daemon failed to restart (no connection after re-init)"
52000
- };
51711
+
51712
+ // src/components/history/code-block.tsx
51713
+ var import_jsx_runtime49 = __toESM(require_jsx_runtime(), 1);
51714
+ var MAX_CODE_LINES = 80;
51715
+ var DIFF_FALLBACK_WRAP_WIDTH = 100;
51716
+ var HARD_TAB_WIDTH = 8;
51717
+ function expandTabs(text, tabWidth = HARD_TAB_WIDTH) {
51718
+ if (!text.includes(" ")) return text;
51719
+ let out = "";
51720
+ let col = 0;
51721
+ for (const ch of text) {
51722
+ if (ch === " ") {
51723
+ const advance = tabWidth - col % tabWidth;
51724
+ out += " ".repeat(advance);
51725
+ col += advance;
51726
+ } else {
51727
+ out += ch;
51728
+ col += 1;
52001
51729
  }
52002
- await connection.request("ping", {}, { timeoutMs: 1e4 });
52003
- return {
52004
- serviceId: "kanban",
52005
- action: "restart",
52006
- success: true,
52007
- message: "Kanban IPC daemon restarted successfully"
52008
- };
52009
- } catch (error) {
52010
- return {
52011
- serviceId: "kanban",
52012
- action: "restart",
52013
- success: false,
52014
- message: `Kanban IPC daemon restarted but verification failed: ${toErrorMessage(error)}`
52015
- };
52016
51730
  }
51731
+ return out;
52017
51732
  }
52018
- async function killSageServer(projectRoot, action) {
52019
- if (!isSageProjectServerAvailable()) {
52020
- return {
52021
- serviceId: "sage",
52022
- action,
52023
- success: false,
52024
- message: "SAGE project server is unavailable in this runtime"
52025
- };
52026
- }
52027
- const connection = new SageProjectServerConnection(projectRoot);
52028
- try {
52029
- const result = await connection.shutdown(`TUI request: ${action}`);
52030
- if (!result.stopped) {
52031
- if (action === "restart" && result.reason === "not-running") {
52032
- return await restartSageServer(projectRoot);
51733
+ function CodeBlockImpl({
51734
+ code,
51735
+ lang,
51736
+ contentWidth
51737
+ }) {
51738
+ useActiveTheme();
51739
+ let lines = sanitizeTerminalText(code, HARD_TAB_WIDTH).replace(/\n+$/, "").split("\n");
51740
+ const hidden = Math.max(0, lines.length - MAX_CODE_LINES);
51741
+ if (hidden > 0) lines = lines.slice(0, MAX_CODE_LINES);
51742
+ const gutterW = String(lines.length).length;
51743
+ const maxW = Math.max(1, Math.min(contentWidth - 4 - gutterW - 1, 120));
51744
+ let carry = {};
51745
+ const rows = lines.map((raw) => {
51746
+ const expanded = expandTabs(raw);
51747
+ const display = truncateDisplay(expanded, maxW);
51748
+ const r = highlightLine(display, lang, carry);
51749
+ carry = r.carry;
51750
+ return r.tokens;
51751
+ });
51752
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(HistoryRail, { color: theme.borderDefault, children: [
51753
+ lang !== "plain" ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: lang }) : null,
51754
+ rows.map((tokens, i) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Text2, { children: [
51755
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: `${String(i + 1).padStart(gutterW, " ")} ` }),
51756
+ tokens.length === 0 ? " " : tokens.map((t, j) => /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
51757
+ Text2,
51758
+ {
51759
+ dimColor: Boolean(t.dim),
51760
+ bold: Boolean(t.bold),
51761
+ ...t.color ? { color: t.color } : {},
51762
+ children: t.text
51763
+ },
51764
+ j
51765
+ ))
51766
+ ] }, i)),
51767
+ hidden > 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, italic: true, children: `\u2026 +${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
51768
+ ] });
51769
+ }
51770
+ function DiffFileBlock({
51771
+ path: path13,
51772
+ preview,
51773
+ useColor = true,
51774
+ contentWidth
51775
+ }) {
51776
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Box2, { flexDirection: "column", marginTop: 1, children: [
51777
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Box2, { flexDirection: "row", marginBottom: 0, children: [
51778
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { bold: true, color: theme.accent, children: `\u25B8 ${path13}` }),
51779
+ preview.added > 0 || preview.removed > 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: ` (${preview.added > 0 ? `+${preview.added}` : ""}${preview.added > 0 && preview.removed > 0 ? " " : ""}${preview.removed > 0 ? `-${preview.removed}` : ""})` }) : null
51780
+ ] }),
51781
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
51782
+ DiffBlock,
51783
+ {
51784
+ rows: preview.rows,
51785
+ hidden: preview.hidden,
51786
+ added: preview.added,
51787
+ removed: preview.removed,
51788
+ hiddenAdded: preview.hiddenAdded,
51789
+ hiddenRemoved: preview.hiddenRemoved,
51790
+ useColor,
51791
+ lang: langFromPath(path13),
51792
+ showStats: false,
51793
+ contentWidth
52033
51794
  }
52034
- return {
52035
- serviceId: "sage",
52036
- action,
52037
- success: false,
52038
- message: `SAGE memory server shutdown failed: ${result.reason ?? "unknown"}`
52039
- };
52040
- }
52041
- if (action === "restart") {
52042
- return await restartSageServer(projectRoot);
52043
- }
52044
- return {
52045
- serviceId: "sage",
52046
- action,
52047
- success: true,
52048
- message: "SAGE memory server shutdown requested"
52049
- };
52050
- } catch (error) {
52051
- return {
52052
- serviceId: "sage",
52053
- action,
52054
- success: false,
52055
- message: toErrorMessage(error)
52056
- };
52057
- } finally {
52058
- connection.close();
52059
- }
51795
+ )
51796
+ ] });
52060
51797
  }
52061
- async function restartSageServer(projectRoot) {
52062
- await waitForShutdown(async () => {
52063
- const probe = new SageProjectServerConnection(projectRoot);
52064
- try {
52065
- return await probe.status() !== null;
52066
- } finally {
52067
- probe.close();
52068
- }
51798
+ function applyWashTokens(tokens, onWash) {
51799
+ if (!onWash) return tokens;
51800
+ return tokens.map((t) => {
51801
+ if (t.color !== "syntax.comment") return t;
51802
+ return { ...t, color: "syntax.commentOnWash", dim: false };
52069
51803
  });
52070
- const verifyConn = new SageProjectServerConnection(projectRoot);
52071
- try {
52072
- await verifyConn.call(
52073
- "ping",
52074
- {},
52075
- { timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } }
52076
- );
52077
- return {
52078
- serviceId: "sage",
52079
- action: "restart",
52080
- success: true,
52081
- message: "SAGE memory server restarted successfully"
52082
- };
52083
- } catch (error) {
52084
- return {
52085
- serviceId: "sage",
52086
- action: "restart",
52087
- success: false,
52088
- message: `SAGE memory server restarted but verification failed: ${toErrorMessage(error)}`
52089
- };
52090
- } finally {
52091
- verifyConn.close();
52092
- }
52093
51804
  }
52094
- async function killChronicleServer(projectRoot, action) {
52095
- const options = resolveChronicleProjectServerOptions({ projectRoot });
52096
- const client = new ChronicleProjectServerClient(options);
52097
- try {
52098
- const result = await client.shutdown(`TUI request: ${action}`);
52099
- if (!result.stopped) {
52100
- if (action === "restart" && result.reason === "offline") {
52101
- return await restartChronicleServer(projectRoot);
51805
+ function renderTokens(tokens, onWash = false) {
51806
+ const effective = applyWashTokens(tokens, onWash);
51807
+ if (effective.length === 0) return " ";
51808
+ return effective.map((t, j) => /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
51809
+ Text2,
51810
+ {
51811
+ dimColor: Boolean(t.dim),
51812
+ bold: Boolean(t.bold),
51813
+ ...t.color ? { color: t.color } : {},
51814
+ children: t.text
51815
+ },
51816
+ j
51817
+ ));
51818
+ }
51819
+ function wrapTokens(tokens, width) {
51820
+ if (width <= 0) return [tokens];
51821
+ const segments = [];
51822
+ let current = [];
51823
+ let currentWidth = 0;
51824
+ for (const t of tokens) {
51825
+ let text = t.text;
51826
+ while (text.length > 0) {
51827
+ const room = width - currentWidth;
51828
+ if (room <= 0) {
51829
+ segments.push(current);
51830
+ current = [];
51831
+ currentWidth = 0;
51832
+ continue;
52102
51833
  }
52103
- return {
52104
- serviceId: "chronicle",
52105
- action,
52106
- success: false,
52107
- message: `Chronicle telemetry server shutdown failed: ${result.reason ?? "unknown"}`
52108
- };
52109
- }
52110
- if (action === "restart") {
52111
- return await restartChronicleServer(projectRoot);
51834
+ const [piece, rest] = splitDisplay(text, room);
51835
+ if (!piece) {
51836
+ segments.push(current);
51837
+ current = [];
51838
+ currentWidth = 0;
51839
+ continue;
51840
+ }
51841
+ current.push({ ...t, text: piece });
51842
+ currentWidth += displayWidth(piece);
51843
+ text = rest;
52112
51844
  }
52113
- return {
52114
- serviceId: "chronicle",
52115
- action,
52116
- success: true,
52117
- message: "Chronicle telemetry server shutdown requested"
52118
- };
52119
- } catch (error) {
52120
- return {
52121
- serviceId: "chronicle",
52122
- action,
52123
- success: false,
52124
- message: toErrorMessage(error)
52125
- };
52126
- } finally {
52127
- client.close();
52128
51845
  }
51846
+ if (current.length > 0 || segments.length === 0) segments.push(current);
51847
+ return segments;
52129
51848
  }
52130
- async function restartChronicleServer(projectRoot) {
52131
- const options = resolveChronicleProjectServerOptions({ projectRoot });
52132
- const endpoint = new ChronicleProjectServerClient(options).endpoint;
52133
- await waitForShutdown(async () => isEndpointAlive(endpoint));
52134
- let access;
52135
- try {
52136
- access = createChronicleProjectAccess({ projectRoot });
52137
- await access.call("ping", {}, { timeoutMs: 1e4 });
52138
- if (access.mode !== "server") {
52139
- return {
52140
- serviceId: "chronicle",
52141
- action: "restart",
52142
- success: false,
52143
- message: `Chronicle telemetry server restarted but running in ${access.mode} mode (expected server)`
52144
- };
52145
- }
52146
- return {
52147
- serviceId: "chronicle",
52148
- action: "restart",
52149
- success: true,
52150
- message: "Chronicle telemetry server restarted successfully"
52151
- };
52152
- } catch (error) {
52153
- return {
52154
- serviceId: "chronicle",
52155
- action: "restart",
52156
- success: false,
52157
- message: `Chronicle telemetry server restarted but verification failed: ${toErrorMessage(error)}`
52158
- };
52159
- } finally {
52160
- await access?.close();
51849
+ function DiffBlock({
51850
+ rows,
51851
+ hidden,
51852
+ added = 0,
51853
+ removed = 0,
51854
+ hiddenAdded = 0,
51855
+ hiddenRemoved = 0,
51856
+ useColor = true,
51857
+ lang = "plain",
51858
+ showStats = true,
51859
+ contentWidth
51860
+ }) {
51861
+ const lineNoOf = (row) => row.kind === "del" ? row.oldLine : row.newLine ?? row.oldLine;
51862
+ let gutterWidth = 1;
51863
+ for (const r of rows) {
51864
+ const n = lineNoOf(r);
51865
+ if (typeof n === "number" && String(n).length > gutterWidth) gutterWidth = String(n).length;
52161
51866
  }
52162
- }
52163
- async function killCodebaseIndexServer(projectRoot, indexDir, action) {
52164
- try {
52165
- const result = await shutdownCodebaseIndexServer(
52166
- projectRoot,
52167
- indexDir,
52168
- `tui-request:${action}`
52169
- );
52170
- if (!result.stopped) {
52171
- if (action === "restart" && result.reason === "not-running") {
52172
- return await restartCodebaseIndexServer(projectRoot, indexDir);
52173
- }
52174
- return {
52175
- serviceId: "codebase-index",
52176
- action,
52177
- success: false,
52178
- message: `Codebase index server shutdown failed: ${result.reason ?? "unknown"}`
52179
- };
52180
- }
52181
- if (action === "restart") {
52182
- return await restartCodebaseIndexServer(projectRoot, indexDir);
51867
+ const blank = " ".repeat(gutterWidth);
51868
+ const markerFor = (kind) => {
51869
+ if (kind === "add") return "+";
51870
+ if (kind === "del") return "-";
51871
+ return " ";
51872
+ };
51873
+ const textForDisplay = (row) => {
51874
+ if ((row.kind === "add" || row.kind === "del" || row.kind === "ctx") && row.text.length > 0) {
51875
+ return sanitizeTerminalText(expandTabs(row.text.slice(1)), HARD_TAB_WIDTH) || " ";
52183
51876
  }
52184
- return {
52185
- serviceId: "codebase-index",
52186
- action,
52187
- success: true,
52188
- message: "Codebase index server shutdown requested"
52189
- };
52190
- } catch (error) {
52191
- return {
52192
- serviceId: "codebase-index",
52193
- action,
52194
- success: false,
52195
- message: toErrorMessage(error)
52196
- };
52197
- }
51877
+ return sanitizeTerminalText(expandTabs(row.text), HARD_TAB_WIDTH) || " ";
51878
+ };
51879
+ const stats = showStats ? formatDiffStats(added, removed) : null;
51880
+ const terminalWrapGuard = typeof contentWidth === "number" ? 1 : 0;
51881
+ const bodyBudget = typeof contentWidth === "number" ? Math.max(1, contentWidth - (2 + 3 + gutterWidth + 3 + terminalWrapGuard)) : DIFF_FALLBACK_WRAP_WIDTH;
51882
+ const hasWidth = typeof contentWidth === "number";
51883
+ const padBody = (seg) => {
51884
+ if (!hasWidth) return 0;
51885
+ const len = seg.reduce((n, t) => n + displayWidth(t.text), 0);
51886
+ return Math.max(0, bodyBudget - len);
51887
+ };
51888
+ const contPrefix = ` ${blank} `;
51889
+ const hiddenStats = [];
51890
+ if (hiddenAdded > 0) hiddenStats.push(`+${hiddenAdded}`);
51891
+ if (hiddenRemoved > 0) hiddenStats.push(`-${hiddenRemoved}`);
51892
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Box2, { flexDirection: "column", marginLeft: 2, marginTop: 0, children: [
51893
+ stats ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Text2, { children: [
51894
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: "\u23BF " }),
51895
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { children: stats })
51896
+ ] }) : null,
51897
+ rows.map((row, i) => {
51898
+ const key2 = i;
51899
+ if (row.kind === "hunk") {
51900
+ if (i === 0) return null;
51901
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: ` ${blank} \u22EF` }, key2);
51902
+ }
51903
+ if (row.kind === "meta") {
51904
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: ` ${blank} ${sanitizeTerminalText(row.text)}` }, key2);
51905
+ }
51906
+ const n = lineNoOf(row);
51907
+ const ln = typeof n === "number" ? String(n).padStart(gutterWidth, " ") : blank;
51908
+ const body = textForDisplay(row);
51909
+ const tokens = highlightLine(body, lang).tokens;
51910
+ const segments = wrapTokens(tokens, bodyBudget);
51911
+ if (row.kind === "ctx") {
51912
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Box2, { flexDirection: "column", children: segments.map((seg, si) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Text2, { children: [
51913
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: si === 0 ? ` ${ln} ` : contPrefix }),
51914
+ renderTokens(seg)
51915
+ ] }, si)) }, key2);
51916
+ }
51917
+ const marker = markerFor(row.kind);
51918
+ const markerColor = row.kind === "add" ? theme.success : theme.error;
51919
+ if (useColor) {
51920
+ const bg = row.kind === "add" ? theme.diffAddBg : theme.diffDelBg;
51921
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Box2, { flexDirection: "column", children: segments.map((seg, si) => {
51922
+ const pad = padBody(seg);
51923
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Text2, { backgroundColor: bg, children: [
51924
+ si === 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_jsx_runtime49.Fragment, { children: [
51925
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: ` ${ln} ` }),
51926
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { color: markerColor, bold: true, children: marker }),
51927
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { children: " " })
51928
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: contPrefix }),
51929
+ renderTokens(seg, true),
51930
+ pad > 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { children: " ".repeat(pad) }) : null
51931
+ ] }, si);
51932
+ }) }, key2);
51933
+ }
51934
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Box2, { flexDirection: "column", children: segments.map((seg, si) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(Text2, { children: [
51935
+ si === 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_jsx_runtime49.Fragment, { children: [
51936
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: ` ${ln} ` }),
51937
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { color: markerColor, bold: true, children: marker }),
51938
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { children: " " })
51939
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, children: contPrefix }),
51940
+ renderTokens(seg)
51941
+ ] }, si)) }, key2);
51942
+ }),
51943
+ hidden > 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Text2, { dimColor: true, italic: true, children: ` ${blank} \u2026 ${hidden} more line${hidden === 1 ? "" : "s"}${hiddenStats.length > 0 ? ` (${hiddenStats.join(" ")} hidden)` : ""}` }) : null
51944
+ ] });
51945
+ }
51946
+ var CodeBlock = (0, import_react62.memo)(CodeBlockImpl);
51947
+
51948
+ // src/components/confirm-prompt.tsx
51949
+ var import_jsx_runtime50 = __toESM(require_jsx_runtime(), 1);
51950
+ var BUTTON_COLOR = {
51951
+ yes: "green",
51952
+ no: "red",
51953
+ always: "cyan",
51954
+ "always-exact": "cyan",
51955
+ "always-command": "cyan",
51956
+ "always-tool": "yellow",
51957
+ deny: "red"
51958
+ };
51959
+ function buttonLabels(_suggestedPattern, toolName = "exec") {
51960
+ return [
51961
+ { decision: "yes", bracket: "[y]", rest: "es " },
51962
+ { decision: "no", bracket: "[n]", rest: "o " },
51963
+ { decision: "always-exact", bracket: "[a]", rest: " same args " },
51964
+ ...toolName === "exec" ? [{ decision: "always-command", bracket: "[c]", rest: " command " }] : [],
51965
+ { decision: "always-tool", bracket: "[t]", rest: " tool " },
51966
+ { decision: "deny", bracket: "[d]", rest: "eny" }
51967
+ ];
52198
51968
  }
52199
- async function restartCodebaseIndexServer(projectRoot, indexDir) {
52200
- await waitForShutdown(async () => {
52201
- try {
52202
- await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
52203
- timeoutMs: 1e3
52204
- });
52205
- return true;
52206
- } catch {
52207
- return false;
52208
- }
52209
- });
52210
- try {
52211
- await ensureCodebaseIndexServer({ projectRoot, indexDir });
52212
- const health2 = await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
52213
- timeoutMs: 1e4
52214
- });
52215
- if (health2.status === "unresponsive") {
52216
- return {
52217
- serviceId: "codebase-index",
52218
- action: "restart",
52219
- success: false,
52220
- message: "Codebase index server restarted but is unresponsive"
52221
- };
52222
- }
52223
- return {
52224
- serviceId: "codebase-index",
52225
- action: "restart",
52226
- success: true,
52227
- message: "Codebase index server restarted successfully"
52228
- };
52229
- } catch (error) {
52230
- return {
52231
- serviceId: "codebase-index",
52232
- action: "restart",
52233
- success: false,
52234
- message: `Codebase index server restarted but verification failed: ${toErrorMessage(error)}`
52235
- };
51969
+ function stringifyInput(input) {
51970
+ if (!input || typeof input !== "object") return "";
51971
+ const obj = input;
51972
+ return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string" && k !== "diff").map(([k, v]) => `${k}: ${truncate(JSON.stringify(v), 80)}`).join(" ");
51973
+ }
51974
+ function truncate(s2, max) {
51975
+ return s2.length <= max ? s2 : `${s2.slice(0, max - 1)}\u2026`;
51976
+ }
51977
+ function payloadDiff(input) {
51978
+ if (!input || typeof input !== "object") return "";
51979
+ const obj = input;
51980
+ const filePath = typeof obj["path"] === "string" ? obj["path"] : "file";
51981
+ if (typeof obj["diff"] === "string" && obj["diff"]) return obj["diff"];
51982
+ const newString = obj["new_string"];
51983
+ if (typeof newString === "string") {
51984
+ const oldString = typeof obj["old_string"] === "string" ? obj["old_string"] : "";
51985
+ return unifiedDiff(oldString, newString, { fromFile: filePath, toFile: filePath, context: 2 });
51986
+ }
51987
+ const content = obj["content"];
51988
+ if (typeof content === "string" && content) {
51989
+ return unifiedDiff("", content, { fromFile: "/dev/null", toFile: filePath, context: 0 });
52236
51990
  }
51991
+ return "";
52237
51992
  }
52238
- async function killMailboxServer(projectRoot, action) {
52239
- if (!isMailboxProjectServerAvailable()) {
52240
- return {
52241
- serviceId: "mailbox",
52242
- action,
52243
- success: false,
52244
- message: "Mailbox project server is unavailable in this runtime"
52245
- };
51993
+ var CONFIRM_DIFF_MAX_LINES = 20;
51994
+ function renderDiffLine(line) {
51995
+ const prefix = line.startsWith("+") ? "green" : line.startsWith("-") ? "red" : line.startsWith("@@") ? "cyan" : void 0;
51996
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(Text2, { ...prefix ? { color: prefix } : {}, children: [
51997
+ line,
51998
+ "\n"
51999
+ ] }, line);
52000
+ }
52001
+ function renderDiff(diff2, path13) {
52002
+ const preview = parseUnifiedDiff(diff2, CONFIRM_DIFF_MAX_LINES);
52003
+ if (preview.rows.length > 0) {
52004
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
52005
+ DiffBlock,
52006
+ {
52007
+ rows: preview.rows,
52008
+ hidden: preview.hidden,
52009
+ added: preview.added,
52010
+ removed: preview.removed,
52011
+ hiddenAdded: preview.hiddenAdded,
52012
+ hiddenRemoved: preview.hiddenRemoved,
52013
+ useColor: theme.supportsBackground,
52014
+ lang: langFromPath(path13 ?? "")
52015
+ }
52016
+ );
52246
52017
  }
52247
- const connection = new MailboxProjectServerConnection(
52248
- resolveWstackPaths2({ projectRoot }).projectDir
52249
- );
52250
- try {
52251
- const result = await connection.shutdown(`TUI request: ${action}`);
52252
- if (!result.stopped) {
52253
- if (action === "restart" && result.reason === "offline") {
52254
- return await restartMailboxServer(projectRoot);
52018
+ const lines = diff2.split("\n").filter((l) => l.length > 0).slice(0, CONFIRM_DIFF_MAX_LINES);
52019
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Box2, { flexDirection: "column", paddingX: 2, children: lines.map((l) => renderDiffLine(l)) });
52020
+ }
52021
+ function ConfirmPrompt({
52022
+ toolName,
52023
+ input,
52024
+ suggestedPattern,
52025
+ onDecision,
52026
+ onEnableYolo,
52027
+ destructive,
52028
+ boundaryReason,
52029
+ writeTargets,
52030
+ maxRows
52031
+ }) {
52032
+ const size = useTerminalSize();
52033
+ const rowBudget = maxRows ?? size.rows;
52034
+ const buttons = import_react63.default.useRef(/* @__PURE__ */ new Map());
52035
+ const partialMouse = import_react63.default.useRef("");
52036
+ const decided = import_react63.default.useRef(false);
52037
+ const decide = (decision) => {
52038
+ if (decided.current) return;
52039
+ decided.current = true;
52040
+ onDecision(decision);
52041
+ };
52042
+ const { stdin } = use_stdin_default();
52043
+ import_react63.default.useEffect(() => {
52044
+ if (!stdin) return;
52045
+ const onData = (data) => {
52046
+ const chunk = splitTrailingMousePartial(partialMouse.current + data.toString());
52047
+ partialMouse.current = chunk.pending;
52048
+ for (const event of parseMouseEvents(chunk.consumed)) {
52049
+ if (event.kind !== "press" || event.button !== "left") continue;
52050
+ for (const [decision, node] of buttons.current) {
52051
+ const rect = measure_element_default(node);
52052
+ if (event.x > rect.x && event.x <= rect.x + rect.width && event.y > rect.y && event.y <= rect.y + rect.height) {
52053
+ decide(decision);
52054
+ break;
52055
+ }
52056
+ }
52255
52057
  }
52256
- return {
52257
- serviceId: "mailbox",
52258
- action,
52259
- success: false,
52260
- message: `Mailbox IPC server shutdown failed: ${result.reason ?? "unknown"}`
52261
- };
52262
- }
52263
- if (action === "restart") {
52264
- return await restartMailboxServer(projectRoot);
52265
- }
52266
- return {
52267
- serviceId: "mailbox",
52268
- action,
52269
- success: true,
52270
- message: "Mailbox IPC server shutdown requested"
52271
52058
  };
52272
- } catch (error) {
52273
- return {
52274
- serviceId: "mailbox",
52275
- action,
52276
- success: false,
52277
- message: toErrorMessage(error)
52059
+ stdin.on("data", onData);
52060
+ return () => {
52061
+ stdin.off("data", onData);
52278
52062
  };
52279
- } finally {
52280
- connection.close();
52281
- }
52282
- }
52283
- async function restartMailboxServer(projectRoot) {
52284
- await waitForShutdown(async () => {
52285
- const probe = new MailboxProjectServerConnection(
52286
- resolveWstackPaths2({ projectRoot }).projectDir
52287
- );
52288
- try {
52289
- return await probe.probeStatus() !== null;
52290
- } finally {
52291
- probe.close();
52063
+ }, [stdin, decide]);
52064
+ import_react63.default.useEffect(() => {
52065
+ writeOut("\x07");
52066
+ }, []);
52067
+ use_input_default((input2, key2) => {
52068
+ if (key2.ctrl || key2.meta || decided.current) return;
52069
+ if (!input2 || input2 === "\r" || input2 === "\n") return;
52070
+ if (input2 === "Y" && !boundaryReason) {
52071
+ onEnableYolo();
52072
+ return;
52073
+ }
52074
+ const ch = input2.toLowerCase();
52075
+ if (ch === "y") {
52076
+ decide("yes");
52077
+ } else if (ch === "n") {
52078
+ decide("no");
52079
+ } else if (ch === "a") {
52080
+ decide("always-exact");
52081
+ } else if (ch === "c" && toolName === "exec") {
52082
+ decide("always-command");
52083
+ } else if (ch === "t") {
52084
+ decide("always-tool");
52085
+ } else if (ch === "d") {
52086
+ decide("deny");
52292
52087
  }
52293
52088
  });
52294
- const verifyConn = new MailboxProjectServerConnection(
52295
- resolveWstackPaths2({ projectRoot }).projectDir
52296
- );
52297
- try {
52298
- await verifyConn.call("ping", {}, { timeoutMs: 1e4 });
52299
- return {
52300
- serviceId: "mailbox",
52301
- action: "restart",
52302
- success: true,
52303
- message: "Mailbox IPC server restarted successfully"
52304
- };
52305
- } catch (error) {
52306
- return {
52307
- serviceId: "mailbox",
52308
- action: "restart",
52309
- success: false,
52310
- message: `Mailbox IPC server restarted but verification failed: ${toErrorMessage(error)}`
52311
- };
52312
- } finally {
52313
- verifyConn.close();
52314
- }
52089
+ const inputSummary = stringifyInput(input);
52090
+ const inp = input;
52091
+ const diff2 = payloadDiff(input);
52092
+ const diffPath = typeof inp?.path === "string" ? inp.path : void 0;
52093
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(PanelInputProvider, { value: true, children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(MonitorViewportProvider, { value: { columns: size.columns, rows: rowBudget }, children: /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
52094
+ MonitorShell,
52095
+ {
52096
+ accent: "yellow",
52097
+ icon: "\u26A0",
52098
+ title: `APPROVAL REQUIRED \xB7 ${toolName}`,
52099
+ footer: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Box2, { flexDirection: "row", flexWrap: "wrap", children: buttonLabels(suggestedPattern, toolName).map((label) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
52100
+ Box2,
52101
+ {
52102
+ flexShrink: 0,
52103
+ ref: (node) => {
52104
+ if (node) buttons.current.set(label.decision, node);
52105
+ else buttons.current.delete(label.decision);
52106
+ },
52107
+ children: /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(Text2, { children: [
52108
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Text2, { bold: true, color: BUTTON_COLOR[label.decision], children: label.bracket }),
52109
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Text2, { dimColor: true, children: label.rest })
52110
+ ] })
52111
+ },
52112
+ label.decision
52113
+ )) }),
52114
+ children: [
52115
+ inputSummary ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Text2, { dimColor: true, children: inputSummary }) : null,
52116
+ boundaryReason ? /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(Text2, { color: "yellow", children: [
52117
+ "KANBAN BOUNDARY: ",
52118
+ boundaryReason
52119
+ ] }) : null,
52120
+ writeTargets && writeTargets.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(Text2, { color: "yellow", children: [
52121
+ "WRITES: ",
52122
+ writeTargets.slice(0, 5).join(", "),
52123
+ writeTargets.length > 5 ? ` (+${writeTargets.length - 5} more)` : ""
52124
+ ] }) : null,
52125
+ diff2 ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Box2, { flexDirection: "column", marginY: 1, children: renderDiff(diff2, diffPath) }) : null,
52126
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(Text2, { dimColor: true, children: [
52127
+ "Remember: [a] this input; ",
52128
+ toolName === "exec" ? "[c] executable, any args; " : "",
52129
+ "[t] tool, any input. Destructive calls still prompt."
52130
+ ] }),
52131
+ !boundaryReason ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Box2, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(Text2, { dimColor: true, children: [
52132
+ " ",
52133
+ "Tip: press",
52134
+ " ",
52135
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(Text2, { bold: true, color: "yellow", children: "Y" }),
52136
+ " ",
52137
+ "to enable YOLO mode",
52138
+ destructive ? " (this destructive approval remains)" : " (skips future routine approvals)",
52139
+ "."
52140
+ ] }) }) : null
52141
+ ]
52142
+ }
52143
+ ) }) });
52315
52144
  }
52316
52145
 
52317
52146
  // src/components/connections-panel.tsx
52147
+ var import_react64 = __toESM(require_react(), 1);
52148
+ init_connection_actions();
52318
52149
  init_connections_health();
52319
52150
  var import_jsx_runtime51 = __toESM(require_jsx_runtime(), 1);
52320
52151
  var REFRESH_MS = 8e3;
@@ -54184,8 +54015,10 @@ var SETTINGS_FIELD_LABELS = [
54184
54015
  // 60
54185
54016
  "Right sidebar",
54186
54017
  // 61
54187
- "Tool result view"
54018
+ "Tool result view",
54188
54019
  // 62
54020
+ "Tool Coach"
54021
+ // 63
54189
54022
  ];
54190
54023
 
54191
54024
  // src/components/model-picker-effort.ts
@@ -55878,6 +55711,7 @@ function resolveSettingsFieldValue(field, input) {
55878
55711
  [10, "featureMemory"],
55879
55712
  [11, "featureSkills"],
55880
55713
  [12, "featureModelsRegistry"],
55714
+ [63, "featureToolCoach"],
55881
55715
  [14, "allowOutsideProjectRoot"],
55882
55716
  [18, "enhanceEnabled"],
55883
55717
  [20, "indexOnStart"],
@@ -56087,6 +55921,7 @@ function getSettingsFieldValue(values, field) {
56087
55921
  [10, "featureMemory"],
56088
55922
  [11, "featureSkills"],
56089
55923
  [12, "featureModelsRegistry"],
55924
+ [63, "featureToolCoach"],
56090
55925
  [14, "allowOutsideProjectRoot"],
56091
55926
  [18, "enhanceEnabled"],
56092
55927
  [20, "indexOnStart"],
@@ -56176,6 +56011,10 @@ var SETTINGS_SECTIONS = [
56176
56011
  name: "Features",
56177
56012
  fields: [8, 9, 10, 11, 12, 13, 14]
56178
56013
  },
56014
+ {
56015
+ name: "Agent guidance",
56016
+ fields: [63]
56017
+ },
56179
56018
  {
56180
56019
  name: "Tools",
56181
56020
  fields: [15, 16, 17, 18, 19, 20, 21, 22, 42, 45]
@@ -56264,6 +56103,7 @@ var SETTINGS_DEFAULTS = Object.freeze({
56264
56103
  featureMemory: true,
56265
56104
  featureSkills: true,
56266
56105
  featureModelsRegistry: true,
56106
+ featureToolCoach: true,
56267
56107
  tokenSavingTier: "off",
56268
56108
  allowOutsideProjectRoot: true,
56269
56109
  contextAutoCompact: true,
@@ -56331,6 +56171,7 @@ function buildResetPatch(field) {
56331
56171
  [10, "featureMemory"],
56332
56172
  [11, "featureSkills"],
56333
56173
  [12, "featureModelsRegistry"],
56174
+ [63, "featureToolCoach"],
56334
56175
  [13, "tokenSavingTier"],
56335
56176
  [14, "allowOutsideProjectRoot"],
56336
56177
  [15, "maxIterations"],
@@ -56525,6 +56366,7 @@ function SettingsPicker({
56525
56366
  featureMemory,
56526
56367
  featureSkills,
56527
56368
  featureModelsRegistry,
56369
+ featureToolCoach,
56528
56370
  tokenSavingTier,
56529
56371
  allowOutsideProjectRoot,
56530
56372
  maxIterations,
@@ -56875,7 +56717,13 @@ function SettingsPicker({
56875
56717
  value: `${boolVal(showSidebar ?? true)}${sidebarPinned ? " \xB7 pinned" : ""}`,
56876
56718
  detail: sidebarPinned ? "A panel is routed to the sidebar \u2014 set that panel to bottom to unlock hiding" : "Show or hide the right sidebar in the TUI (chat history takes full width when off)"
56877
56719
  },
56878
- toolResultViewSettingRow(toolResultViewMode)
56720
+ toolResultViewSettingRow(toolResultViewMode),
56721
+ { section: "Agent guidance" },
56722
+ {
56723
+ label: "Tool Coach",
56724
+ value: boolVal(featureToolCoach),
56725
+ detail: "Suggest enabled tools and guide recovery after tool errors (default on)"
56726
+ }
56879
56727
  ];
56880
56728
  const fieldRowIndex = [];
56881
56729
  for (let i = 0; i < rows.length; i++) {
@@ -57460,8 +57308,86 @@ function SubagentModelsPanel({
57460
57308
  ] });
57461
57309
  }
57462
57310
 
57311
+ // src/theme-picker-rows.ts
57312
+ function filterThemeOptions(options, query) {
57313
+ const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
57314
+ if (terms.length === 0) return options;
57315
+ return options.filter((opt) => {
57316
+ const haystack = `${opt.name} ${opt.id} ${opt.description}`.toLowerCase();
57317
+ return terms.every((term) => haystack.includes(term));
57318
+ });
57319
+ }
57320
+ function buildThemeRows(options) {
57321
+ const rows = [];
57322
+ let lastFamily;
57323
+ for (const [index, option] of options.entries()) {
57324
+ if (option.family !== lastFamily) {
57325
+ rows.push({ kind: "header", family: option.family });
57326
+ lastFamily = option.family;
57327
+ }
57328
+ rows.push({ kind: "option", option, index });
57329
+ }
57330
+ return rows;
57331
+ }
57332
+ function windowThemeRows(rows, selected, budget) {
57333
+ if (rows.length === 0) return { rows: [], hasAbove: false, hasBelow: false };
57334
+ const focusPos = Math.max(
57335
+ 0,
57336
+ rows.findIndex((r) => r.kind === "option" && r.index === selected)
57337
+ );
57338
+ let headerPos = focusPos;
57339
+ for (let i = focusPos; i >= 0; i -= 1) {
57340
+ if (rows[i].kind === "header") {
57341
+ headerPos = i;
57342
+ break;
57343
+ }
57344
+ }
57345
+ const capacity = Math.max(2, budget);
57346
+ let start = focusPos + 1 - headerPos <= capacity ? headerPos : focusPos;
57347
+ let end = focusPos + 1;
57348
+ let used = end - start;
57349
+ while (used < capacity && end < rows.length) {
57350
+ const cost = needsHeaderAbove(rows, end) ? 2 : 1;
57351
+ if (used + cost > capacity) break;
57352
+ end += cost;
57353
+ used += cost;
57354
+ }
57355
+ while (used < capacity && start > 0) {
57356
+ start -= 1;
57357
+ used += 1;
57358
+ }
57359
+ return {
57360
+ rows: rows.slice(start, end),
57361
+ hasAbove: start > 0,
57362
+ hasBelow: end < rows.length
57363
+ };
57364
+ }
57365
+ function needsHeaderAbove(rows, pos) {
57366
+ const row = rows[pos];
57367
+ if (row === void 0) return false;
57368
+ return row.kind === "header" || rows[pos - 1]?.kind === "header";
57369
+ }
57370
+
57463
57371
  // src/components/theme-preview.tsx
57464
57372
  var import_jsx_runtime81 = __toESM(require_jsx_runtime(), 1);
57373
+ var CONTRAST_FLOOR = 3;
57374
+ function ContrastRow({
57375
+ fg,
57376
+ bg,
57377
+ label
57378
+ }) {
57379
+ const ratio = contrastRatio(fg, bg);
57380
+ const weak = ratio < CONTRAST_FLOOR;
57381
+ return /* @__PURE__ */ (0, import_jsx_runtime81.jsxs)(Text2, { children: [
57382
+ " ",
57383
+ /* @__PURE__ */ (0, import_jsx_runtime81.jsx)(Text2, { dimColor: true, children: label.padEnd(5) }),
57384
+ /* @__PURE__ */ (0, import_jsx_runtime81.jsx)(Text2, { color: weak ? "yellow" : void 0, children: weak ? "\u25B2" : "\xB7" }),
57385
+ /* @__PURE__ */ (0, import_jsx_runtime81.jsxs)(Text2, { color: weak ? "yellow" : void 0, bold: weak, children: [
57386
+ ratio.toFixed(1),
57387
+ ":1"
57388
+ ] })
57389
+ ] });
57390
+ }
57465
57391
  function ThemePreview({
57466
57392
  presetId,
57467
57393
  active,
@@ -57567,6 +57493,21 @@ function ThemePreview({
57567
57493
  /* @__PURE__ */ (0, import_jsx_runtime81.jsx)(Text2, { backgroundColor: preset.diffDelBg, children: " " })
57568
57494
  ] })
57569
57495
  ] })
57496
+ },
57497
+ {
57498
+ // Placed LAST so the existing trim-from-bottom order is untouched: a
57499
+ // short terminal drops contrast before it drops the sample/diff the user
57500
+ // actually judges the palette by.
57501
+ key: "contrast",
57502
+ // Label + three readings = 4 physical rows. The cost must match the real
57503
+ // height or the trim loop lets the preview overrun the picker's budget.
57504
+ cost: 4,
57505
+ render: /* @__PURE__ */ (0, import_jsx_runtime81.jsxs)(Box2, { flexDirection: "column", children: [
57506
+ /* @__PURE__ */ (0, import_jsx_runtime81.jsx)(Text2, { dimColor: true, children: "contrast" }),
57507
+ /* @__PURE__ */ (0, import_jsx_runtime81.jsx)(ContrastRow, { fg: preset.textPrimary, bg: preset.surface, label: "text" }),
57508
+ /* @__PURE__ */ (0, import_jsx_runtime81.jsx)(ContrastRow, { fg: preset.success, bg: preset.diffAddBg, label: "+add" }),
57509
+ /* @__PURE__ */ (0, import_jsx_runtime81.jsx)(ContrastRow, { fg: preset.error, bg: preset.diffDelBg, label: "-del" })
57510
+ ] })
57570
57511
  }
57571
57512
  ];
57572
57513
  let used = 0;
@@ -57595,21 +57536,21 @@ function ThemePicker({
57595
57536
  activeId,
57596
57537
  hint,
57597
57538
  columns = 0,
57598
- maxRows
57539
+ maxRows,
57540
+ filter = ""
57599
57541
  }) {
57600
57542
  const split = columns >= SPLIT_MIN_COLUMNS && options.length > 0 && (maxRows === void 0 || maxRows >= PREVIEW_MIN_ROWS);
57601
57543
  const listMaxRows = split && maxRows !== void 0 ? Math.min(maxRows, PREVIEW_FULL_ROWS) : maxRows;
57602
- const { start, end, hasAbove, hasBelow } = useWindowedPicker({
57603
- total: options.length,
57604
- selected,
57605
- chromeRows: 4,
57606
- // 2 marker slots + 1 conditional hint slot — reserve the worst case so
57607
- // the window never overflows whether or not those rows actually render.
57608
- markerRows: 3,
57609
- maxRows: listMaxRows
57610
- });
57611
- const visibleOptions = options.slice(start, end);
57612
- const selectedOption = options[Math.max(0, Math.min(selected, options.length - 1))];
57544
+ const visibleOptions = filterThemeOptions(options, filter);
57545
+ const rows = buildThemeRows(visibleOptions);
57546
+ const { rows: rows_terminal } = useTerminalSize();
57547
+ const chromeRows = 4;
57548
+ const markerRows = 4;
57549
+ const listBudget = listMaxRows === void 0 ? Math.max(1, rows_terminal - chromeRows - markerRows - SHELL_RESERVED_ROWS) : Math.max(1, listMaxRows - chromeRows - markerRows);
57550
+ const safeSelected = Math.max(0, Math.min(selected, Math.max(0, visibleOptions.length - 1)));
57551
+ const window2 = windowThemeRows(rows, safeSelected, listBudget);
57552
+ const selectedOption = visibleOptions[safeSelected];
57553
+ const innerWidth = (split ? LIST_COLUMN_WIDTH : Math.max(0, columns)) - LIST_CHROME_COLUMNS;
57613
57554
  const list = /* @__PURE__ */ (0, import_jsx_runtime82.jsxs)(
57614
57555
  Box2,
57615
57556
  {
@@ -57620,40 +57561,19 @@ function ThemePicker({
57620
57561
  ...split ? { width: LIST_COLUMN_WIDTH, flexShrink: 0 } : {},
57621
57562
  children: [
57622
57563
  /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Text2, { color: "cyan", bold: true, children: "\u2501\u2501 TUI Theme \u2501\u2501" }),
57623
- /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Text2, { dimColor: true, wrap: "truncate-end", children: "\u2191\u2193 \xB7 Enter apply \xB7 Esc cancel" }),
57624
- hasAbove ? /* @__PURE__ */ (0, import_jsx_runtime82.jsxs)(Text2, { dimColor: true, children: [
57625
- " \u2026 ",
57626
- start,
57627
- " more above"
57628
- ] }) : null,
57629
- visibleOptions.map((opt, j) => {
57630
- const i = start + j;
57631
- const isActive = opt.id === activeId;
57632
- const isSelected = i === selected;
57633
- return /* @__PURE__ */ (0, import_jsx_runtime82.jsxs)(
57634
- Text2,
57635
- {
57636
- wrap: "truncate-end",
57637
- inverse: isSelected,
57638
- ...isSelected ? { color: "cyan" } : {},
57639
- children: [
57640
- isSelected ? "\u203A " : " ",
57641
- /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Text2, { bold: true, children: opt.name.padEnd(21) }),
57642
- isActive ? /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Text2, { color: "green", children: " [active]" }) : null,
57643
- split ? null : /* @__PURE__ */ (0, import_jsx_runtime82.jsxs)(Text2, { dimColor: true, children: [
57644
- " ",
57645
- opt.description
57646
- ] })
57647
- ]
57648
- },
57649
- opt.id
57650
- );
57651
- }),
57652
- hasBelow ? /* @__PURE__ */ (0, import_jsx_runtime82.jsxs)(Text2, { dimColor: true, children: [
57653
- " \u2026 ",
57654
- options.length - end,
57655
- " more below"
57564
+ /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Text2, { dimColor: true, wrap: "truncate-end", children: "\u2191\u2193 Enter apply \xB7 / \xB7 u undo \xB7 Esc" }),
57565
+ filter ? /* @__PURE__ */ (0, import_jsx_runtime82.jsxs)(Text2, { color: "yellow", wrap: "truncate-end", children: [
57566
+ "/",
57567
+ filter,
57568
+ visibleOptions.length,
57569
+ " match",
57570
+ visibleOptions.length === 1 ? "" : "es"
57656
57571
  ] }) : null,
57572
+ window2.hasAbove ? /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Text2, { dimColor: true, children: " \u2026 more above" }) : null,
57573
+ window2.rows.map(
57574
+ (row) => renderRow2(row, keyFor(row), activeId, safeSelected, split, innerWidth)
57575
+ ),
57576
+ window2.hasBelow ? /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Text2, { dimColor: true, children: " \u2026 more below" }) : null,
57657
57577
  hint ? /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Text2, { color: "yellow", wrap: "truncate-end", children: hint }) : null
57658
57578
  ]
57659
57579
  }
@@ -57674,8 +57594,67 @@ function ThemePicker({
57674
57594
  )
57675
57595
  ] });
57676
57596
  }
57677
- var LIST_COLUMN_WIDTH = 36;
57678
- var SPLIT_MIN_COLUMNS = 88;
57597
+ function keyFor(row) {
57598
+ return row.kind === "header" ? `h:${row.family}` : `o:${row.option.id}`;
57599
+ }
57600
+ function renderRow2(row, key2, activeId, selected, split, innerWidth) {
57601
+ if (row.kind === "header") {
57602
+ return /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Text2, { color: "blue", bold: true, dimColor: true, wrap: "truncate-end", children: row.family }, key2);
57603
+ }
57604
+ const opt = row.option;
57605
+ const isActive = opt.id === activeId;
57606
+ const isSelected = row.index === selected;
57607
+ const fixed = ROW_BASE_COLUMNS + (isActive ? ACTIVE_MARKER_COLUMNS : 0);
57608
+ const showSwatches = !split && innerWidth - fixed >= SWATCH_COLUMNS;
57609
+ const showDescription = !split && innerWidth - fixed - (showSwatches ? SWATCH_COLUMNS : 0) >= MIN_DESCRIPTION_COLUMNS;
57610
+ return (
57611
+ // The cursor/name/marker run and the swatches are SIBLINGS, not nested.
57612
+ // Ink propagates a parent's `inverse` to nested children, so keeping
57613
+ // <Swatches/> inside the inverted <Text> inverted every chip — each swatch
57614
+ // would render in the complement of its own colour, which defeats the
57615
+ // point of sampling the palette.
57616
+ /* @__PURE__ */ (0, import_jsx_runtime82.jsxs)(Box2, { flexDirection: "row", children: [
57617
+ /* @__PURE__ */ (0, import_jsx_runtime82.jsxs)(Text2, { inverse: isSelected, ...isSelected ? { color: "cyan" } : {}, children: [
57618
+ isSelected ? "\u203A " : " ",
57619
+ /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Text2, { bold: true, children: fitName(opt.name) }),
57620
+ isActive ? /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Text2, { color: "green", children: " [active]" }) : null
57621
+ ] }),
57622
+ showSwatches ? /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Swatches, { optionId: opt.id }) : null,
57623
+ showDescription ? /* @__PURE__ */ (0, import_jsx_runtime82.jsxs)(Text2, { dimColor: true, wrap: "truncate-end", children: [
57624
+ " ",
57625
+ opt.description
57626
+ ] }) : null
57627
+ ] }, key2)
57628
+ );
57629
+ }
57630
+ var NAME_COLUMNS = 21;
57631
+ function fitName(name) {
57632
+ return name.length > NAME_COLUMNS ? `${name.slice(0, NAME_COLUMNS - 1)}\u2026` : name.padEnd(NAME_COLUMNS);
57633
+ }
57634
+ var SWATCH_COLUMNS = 7;
57635
+ var ROW_BASE_COLUMNS = 2 + NAME_COLUMNS;
57636
+ var ACTIVE_MARKER_COLUMNS = 9;
57637
+ var MIN_DESCRIPTION_COLUMNS = 12;
57638
+ var LIST_CHROME_COLUMNS = 4;
57639
+ function Swatches({ optionId }) {
57640
+ const preset = themePresets[optionId];
57641
+ if (!preset) return null;
57642
+ const chips = [
57643
+ preset.accent,
57644
+ preset.brandPrimary,
57645
+ preset.user,
57646
+ preset.success,
57647
+ preset.error,
57648
+ preset.surfaceRaised
57649
+ ];
57650
+ return /* @__PURE__ */ (0, import_jsx_runtime82.jsxs)(Text2, { children: [
57651
+ " ",
57652
+ chips.map((color, i) => /* @__PURE__ */ (0, import_jsx_runtime82.jsx)(Text2, { backgroundColor: color, children: " " }, `${optionId}-${i}`))
57653
+ ] });
57654
+ }
57655
+ var SHELL_RESERVED_ROWS = 6;
57656
+ var LIST_COLUMN_WIDTH = 43;
57657
+ var SPLIT_MIN_COLUMNS = 92;
57679
57658
  var PREVIEW_MIN_ROWS = 12;
57680
57659
  var PREVIEW_FULL_ROWS = 18;
57681
57660
 
@@ -58094,6 +58073,7 @@ function AppViewPickers({
58094
58073
  selected: state.themePicker.selected,
58095
58074
  activeId: getActiveThemeName(),
58096
58075
  hint: state.themePicker.hint,
58076
+ filter: state.themePicker.filter,
58097
58077
  columns: mainColumnWidth,
58098
58078
  maxRows: pickerMaxRows
58099
58079
  }
@@ -58177,6 +58157,7 @@ function AppViewPickers({
58177
58157
  featureMemory: state.settingsPicker.featureMemory,
58178
58158
  featureSkills: state.settingsPicker.featureSkills,
58179
58159
  featureModelsRegistry: state.settingsPicker.featureModelsRegistry,
58160
+ featureToolCoach: state.settingsPicker.featureToolCoach,
58180
58161
  tokenSavingTier: state.settingsPicker.tokenSavingTier,
58181
58162
  allowOutsideProjectRoot: state.settingsPicker.allowOutsideProjectRoot,
58182
58163
  contextAutoCompact: state.settingsPicker.contextAutoCompact,
@@ -62072,7 +62053,7 @@ function ComposerTopRail({
62072
62053
  function inputContentWidth(termColumns) {
62073
62054
  return Math.max(8, termColumns - 4);
62074
62055
  }
62075
- function renderRow2(cells, rowKey, promptColor) {
62056
+ function renderRow3(cells, rowKey, promptColor) {
62076
62057
  const out = [];
62077
62058
  let run = "";
62078
62059
  let runStart = 0;
@@ -62401,7 +62382,7 @@ var Input = (0, import_react78.memo)(function Input2({
62401
62382
  const padding = " ".repeat(Math.max(0, contentWidth - rowWidth));
62402
62383
  return /* @__PURE__ */ (0, import_jsx_runtime104.jsxs)(Text2, { children: [
62403
62384
  /* @__PURE__ */ (0, import_jsx_runtime104.jsx)(Text2, { color: disabled ? theme.error : accent ?? theme.brandPrimary, children: "\u2502 " }),
62404
- row.length === 0 ? null : renderRow2(row, `r${i}`, promptColor),
62385
+ row.length === 0 ? null : renderRow3(row, `r${i}`, promptColor),
62405
62386
  /* @__PURE__ */ (0, import_jsx_runtime104.jsx)(Text2, { children: padding }),
62406
62387
  /* @__PURE__ */ (0, import_jsx_runtime104.jsx)(Text2, { color: disabled ? theme.error : accent ?? theme.brandPrimary, children: " \u2502" })
62407
62388
  ] }, i);
@@ -62902,7 +62883,7 @@ var EntryHeightCache = class {
62902
62883
  };
62903
62884
 
62904
62885
  // src/hit-test.ts
62905
- var SCROLLBAR_HIT_WIDTH = 5;
62886
+ var SCROLLBAR_HIT_WIDTH = 8;
62906
62887
  function historyViewportRows(termRows, bottomHeight) {
62907
62888
  return Math.max(1, Math.floor(termRows) - Math.max(0, Math.floor(bottomHeight)));
62908
62889
  }
@@ -63702,10 +63683,10 @@ function renderTable(tableLines, maxWidth) {
63702
63683
  const widths = computeWidths([header, ...dataRows], cols, maxWidth, sepWidths);
63703
63684
  const lines = [];
63704
63685
  lines.push(border("\u250C", "\u252C", "\u2510", widths));
63705
- lines.push(...renderRow3(header, widths, aligns));
63686
+ lines.push(...renderRow4(header, widths, aligns));
63706
63687
  lines.push(border("\u251C", "\u253C", "\u2524", widths));
63707
63688
  for (const row of dataRows) {
63708
- lines.push(...renderRow3(row, widths, aligns));
63689
+ lines.push(...renderRow4(row, widths, aligns));
63709
63690
  }
63710
63691
  lines.push(border("\u2514", "\u2534", "\u2518", widths));
63711
63692
  return lines.join("\n");
@@ -63771,9 +63752,12 @@ function border(left, mid, right, widths) {
63771
63752
  return left + widths.map((w) => "\u2500".repeat(w + 2)).join(mid) + right;
63772
63753
  }
63773
63754
  function stripInlineMarkers(text) {
63774
- return text.replace(/\*\*(.+?)\*\*/g, "$1").replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "$1").replace(/`(.+?)`/g, "$1").replace(/~~(.+?)~~/g, "$1");
63755
+ return text.split(/(`.+?`)/).map((part, i) => i % 2 === 1 ? part.slice(1, -1) : stripEmphasis(part)).join("");
63775
63756
  }
63776
- function renderRow3(cells, widths, aligns) {
63757
+ function stripEmphasis(text) {
63758
+ return text.replace(/\*\*(?!\s)(.+?)(?<!\s)\*\*/g, "$1").replace(/(?<!\*)\*(?![\s*])(.+?)(?<![\s*])\*(?!\*)/g, "$1").replace(/~~(.+?)~~/g, "$1");
63759
+ }
63760
+ function renderRow4(cells, widths, aligns) {
63777
63761
  const safe = cells.map((cell) => breakLigatures(stripInlineMarkers(cell)));
63778
63762
  const wrapped = safe.map((c, i) => wrapCell(c, widths[i] ?? MIN_COL_WIDTH));
63779
63763
  const height = Math.max(1, ...wrapped.map((w) => w.length));
@@ -63804,7 +63788,7 @@ function wrapCell(text, width) {
63804
63788
  continue;
63805
63789
  }
63806
63790
  if (cur) {
63807
- out.push(padVisual(cur, width));
63791
+ out.push(clipVisual(cur.trimEnd(), width));
63808
63792
  cur = "";
63809
63793
  curWidth = 0;
63810
63794
  }
@@ -63814,7 +63798,7 @@ function wrapCell(text, width) {
63814
63798
  while (restWidth > width) {
63815
63799
  const [collected, remaining] = splitDisplay(rest, width);
63816
63800
  if (collected === "") break;
63817
- out.push(padVisual(collected, width));
63801
+ out.push(clipVisual(collected, width));
63818
63802
  rest = remaining;
63819
63803
  restWidth = strWidth(rest);
63820
63804
  }
@@ -63825,13 +63809,11 @@ function wrapCell(text, width) {
63825
63809
  curWidth = wordWidth;
63826
63810
  }
63827
63811
  }
63828
- if (cur) out.push(padVisual(cur, width));
63812
+ if (cur) out.push(clipVisual(cur.trimEnd(), width));
63829
63813
  return out.length === 0 ? [""] : out;
63830
63814
  }
63831
- function padVisual(text, targetWidth) {
63832
- const w = strWidth(text);
63833
- if (w >= targetWidth) return truncateDisplay(text, targetWidth, "");
63834
- return text + " ".repeat(targetWidth - w);
63815
+ function clipVisual(text, targetWidth) {
63816
+ return strWidth(text) > targetWidth ? truncateDisplay(text, targetWidth, "") : text;
63835
63817
  }
63836
63818
  function padCell(text, width, align) {
63837
63819
  const visualLen = strWidth(text);
@@ -65764,6 +65746,7 @@ var Scrollbar = (0, import_react88.memo)(function Scrollbar2({
65764
65746
  ),
65765
65747
  /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(Text2, { children: " " }),
65766
65748
  /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(Text2, { color: copyHit?.inspectCol !== void 0 ? theme.accent : void 0, children: copyHit?.inspectCol !== void 0 ? INSPECT_ICON : " " }),
65749
+ /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(Text2, { children: " " }),
65767
65750
  /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(Text2, { ...inBand || row === markedRow ? { color: theme.accent } : {}, children: isHead ? "\u2588" : inBand ? "\u258C" : row === markedRow ? "\u25C0" : " " }),
65768
65751
  /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
65769
65752
  Text2,
@@ -66529,7 +66512,10 @@ function closePanels(state) {
66529
66512
  sddBoard: state.sddBoard ? { ...state.sddBoard, monitorOpen: false } : state.sddBoard,
66530
66513
  worktreeMonitorOpen: false,
66531
66514
  coordinator: { ...state.coordinator, monitorOpen: false },
66532
- themePicker: { ...state.themePicker, open: false },
66515
+ // `preview` is explicitly cleared: the spread would carry a live-preview
66516
+ // flag into the next `/theme` open, so arrows would start applying themes
66517
+ // with no keypress opting in.
66518
+ themePicker: { ...state.themePicker, open: false, preview: false },
66533
66519
  skillPicker: { ...state.skillPicker, open: false },
66534
66520
  resourceMenu: { ...state.resourceMenu, open: false, pendingAction: void 0 }
66535
66521
  };
@@ -68286,6 +68272,7 @@ function useSettingsAutoSave(state, saveSettings, dispatch) {
68286
68272
  featureMemory: sp.featureMemory,
68287
68273
  featureSkills: sp.featureSkills,
68288
68274
  featureModelsRegistry: sp.featureModelsRegistry,
68275
+ featureToolCoach: sp.featureToolCoach,
68289
68276
  featureTokenSaving: sp.tokenSavingTier,
68290
68277
  allowOutsideProjectRoot: sp.allowOutsideProjectRoot,
68291
68278
  contextAutoCompact: sp.contextAutoCompact,
@@ -68353,6 +68340,7 @@ function useSettingsAutoSave(state, saveSettings, dispatch) {
68353
68340
  state.settingsPicker.featureMemory,
68354
68341
  state.settingsPicker.featureSkills,
68355
68342
  state.settingsPicker.featureModelsRegistry,
68343
+ state.settingsPicker.featureToolCoach,
68356
68344
  state.settingsPicker.tokenSavingTier,
68357
68345
  state.settingsPicker.allowOutsideProjectRoot,
68358
68346
  state.settingsPicker.contextAutoCompact,
@@ -68570,6 +68558,7 @@ function useStreamChipExpiration({
68570
68558
 
68571
68559
  // src/hooks/use-theme-state.ts
68572
68560
  var import_react102 = __toESM(require_react(), 1);
68561
+ import { THEME_RANDOM_ID } from "@wrongstack/core/types";
68573
68562
  function useThemeState({ configStore }) {
68574
68563
  (0, import_react102.useEffect)(() => {
68575
68564
  let randomMode = false;
@@ -68589,7 +68578,7 @@ function useThemeState({ configStore }) {
68589
68578
  }, RANDOM_THEME_ROTATION_MS);
68590
68579
  };
68591
68580
  const syncTheme = (preset) => {
68592
- if (!preset) {
68581
+ if (!preset || preset === THEME_RANDOM_ID) {
68593
68582
  startRandomMode();
68594
68583
  return;
68595
68584
  }
@@ -70140,6 +70129,7 @@ function createInitialState(options) {
70140
70129
  featureMemory: true,
70141
70130
  featureSkills: true,
70142
70131
  featureModelsRegistry: true,
70132
+ featureToolCoach: true,
70143
70133
  tokenSavingTier: "off",
70144
70134
  allowOutsideProjectRoot: true,
70145
70135
  contextAutoCompact: true,
@@ -70735,6 +70725,11 @@ var composerActionTypes = [
70735
70725
  "themePickerClose",
70736
70726
  "themePickerMove",
70737
70727
  "themePickerHint",
70728
+ "themePickerFilter",
70729
+ "themePickerFilterMode",
70730
+ "themePickerPreview",
70731
+ "themePickerUndo",
70732
+ "themePickerSwapPrevious",
70738
70733
  "modePickerOpen",
70739
70734
  "modePickerClose",
70740
70735
  "modePickerMove",
@@ -70926,7 +70921,14 @@ function reduceComposerPickers(state, action) {
70926
70921
  return {
70927
70922
  ...state,
70928
70923
  ...closePanels(state),
70929
- themePicker: { open: true, selected, hint: void 0 }
70924
+ themePicker: {
70925
+ open: true,
70926
+ selected,
70927
+ hint: void 0,
70928
+ filter: "",
70929
+ filtering: false,
70930
+ previous: getActiveThemeName()
70931
+ }
70930
70932
  };
70931
70933
  }
70932
70934
  case "themePickerClose":
@@ -70935,7 +70937,9 @@ function reduceComposerPickers(state, action) {
70935
70937
  themePicker: { open: false, selected: 0 }
70936
70938
  };
70937
70939
  case "themePickerMove": {
70938
- const n = THEME_OPTIONS.length;
70940
+ const visible = filterThemeOptions(THEME_OPTIONS, state.themePicker.filter ?? "");
70941
+ const n = visible.length;
70942
+ if (n === 0) return state;
70939
70943
  const next = (state.themePicker.selected + action.delta + n) % n;
70940
70944
  if (next === state.themePicker.selected) return state;
70941
70945
  return {
@@ -70948,6 +70952,57 @@ function reduceComposerPickers(state, action) {
70948
70952
  ...state,
70949
70953
  themePicker: { ...state.themePicker, hint: action.text }
70950
70954
  };
70955
+ case "themePickerPreview":
70956
+ return {
70957
+ ...state,
70958
+ themePicker: {
70959
+ ...state.themePicker,
70960
+ preview: action.on,
70961
+ // Entering preview re-baselines `previous` to whatever is live RIGHT
70962
+ // NOW, so Esc restores the palette that was in effect when preview
70963
+ // began — not the one from when the picker was first opened.
70964
+ ...action.on ? { previous: getActiveThemeName() } : {}
70965
+ }
70966
+ };
70967
+ case "themePickerFilterMode":
70968
+ return {
70969
+ ...state,
70970
+ themePicker: {
70971
+ ...state.themePicker,
70972
+ filtering: action.on,
70973
+ // Leaving filter mode with `/` also drops the query, so the mode
70974
+ // flag and the visible list can never disagree about what is shown.
70975
+ filter: action.on ? state.themePicker.filter : ""
70976
+ }
70977
+ };
70978
+ case "themePickerFilter": {
70979
+ const previousId = filterThemeOptions(THEME_OPTIONS, state.themePicker.filter ?? "")[state.themePicker.selected]?.id;
70980
+ const visible = filterThemeOptions(THEME_OPTIONS, action.text);
70981
+ const retained = visible.findIndex((o) => o.id === previousId);
70982
+ return {
70983
+ ...state,
70984
+ themePicker: {
70985
+ ...state.themePicker,
70986
+ filter: action.text,
70987
+ selected: retained >= 0 ? retained : 0,
70988
+ hint: void 0
70989
+ }
70990
+ };
70991
+ }
70992
+ case "themePickerUndo":
70993
+ return state;
70994
+ case "themePickerSwapPrevious": {
70995
+ const visible = filterThemeOptions(THEME_OPTIONS, state.themePicker.filter ?? "");
70996
+ const activeIndex = visible.findIndex((o) => o.id === getActiveThemeName());
70997
+ return {
70998
+ ...state,
70999
+ themePicker: {
71000
+ ...state.themePicker,
71001
+ previous: getActiveThemeName(),
71002
+ ...activeIndex >= 0 ? { selected: activeIndex } : {}
71003
+ }
71004
+ };
71005
+ }
70951
71006
  case "modePickerOpen":
70952
71007
  return {
70953
71008
  ...state,
@@ -71357,6 +71412,11 @@ function reduceComposer(state, action) {
71357
71412
  case "themePickerClose":
71358
71413
  case "themePickerMove":
71359
71414
  case "themePickerHint":
71415
+ case "themePickerFilter":
71416
+ case "themePickerFilterMode":
71417
+ case "themePickerPreview":
71418
+ case "themePickerUndo":
71419
+ case "themePickerSwapPrevious":
71360
71420
  case "modePickerOpen":
71361
71421
  case "modePickerClose":
71362
71422
  case "modePickerMove":
@@ -73257,6 +73317,7 @@ function reduceSettingsPanel(state, action) {
73257
73317
  featureMemory: action.featureMemory,
73258
73318
  featureSkills: action.featureSkills,
73259
73319
  featureModelsRegistry: action.featureModelsRegistry,
73320
+ featureToolCoach: action.featureToolCoach,
73260
73321
  tokenSavingTier: action.tokenSavingTier,
73261
73322
  allowOutsideProjectRoot: action.allowOutsideProjectRoot,
73262
73323
  contextAutoCompact: action.contextAutoCompact,
@@ -73851,6 +73912,11 @@ function reduceSettingsValues(state, action) {
73851
73912
  hint: void 0
73852
73913
  }
73853
73914
  };
73915
+ if (f === 63)
73916
+ return {
73917
+ ...state,
73918
+ settingsPicker: { ...sp, featureToolCoach: !sp.featureToolCoach, hint: void 0 }
73919
+ };
73854
73920
  if (f >= PANEL_POSITION_FIELD_START && f - PANEL_POSITION_FIELD_START < PANEL_IDS.length) {
73855
73921
  const PANEL_POSITION_CYCLE = ["bottom", "sidebar"];
73856
73922
  const panelId = PANEL_IDS[f - PANEL_POSITION_FIELD_START];
@@ -75564,18 +75630,50 @@ function createChatSearchSlashCommand(deps) {
75564
75630
 
75565
75631
  // src/connections-slash.ts
75566
75632
  function createConnectionsSlashCommand(deps) {
75633
+ let restarting = false;
75567
75634
  return {
75568
75635
  name: "connections",
75569
75636
  aliases: ["conn", "conns"],
75570
75637
  description: "Show service connection health and restart daemons \u2014 Session Catalog, Chronicle, Codebase Index, SAGE, Kanban, Mailbox, Governance.",
75571
- argsHint: "[open]",
75638
+ argsHint: "[open|restart]",
75572
75639
  category: "Inspect",
75573
- help: "Usage:\n /connections \u2014 open the interactive service health and restart panel\n /connections open \u2014 same as above\n",
75574
- async run(args) {
75640
+ help: "Usage:\n /connections \u2014 open the interactive service health and restart panel\n /connections open \u2014 same as above\n /connections restart \u2014 restart all six supported project daemons without opening the panel\nGovernance is read-only and is excluded from restart.\n",
75641
+ async run(args, ctx) {
75575
75642
  const trimmed = args.trim().toLowerCase();
75643
+ if (trimmed === "restart") {
75644
+ if (!ctx?.projectRoot) return { message: "No active project; cannot restart daemons." };
75645
+ if (restarting) return { message: "Connection daemon restart is already in progress." };
75646
+ restarting = true;
75647
+ try {
75648
+ const { executeConnectionAction: executeConnectionAction2, RESTARTABLE_SERVICES: RESTARTABLE_SERVICES2 } = await Promise.resolve().then(() => (init_connection_actions(), connection_actions_exports));
75649
+ const projectRoot = ctx.projectRoot;
75650
+ const lines = [];
75651
+ let succeeded = 0;
75652
+ for (const serviceId of RESTARTABLE_SERVICES2) {
75653
+ try {
75654
+ const result = await executeConnectionAction2(serviceId, "restart", projectRoot);
75655
+ if (result.success) succeeded++;
75656
+ lines.push(`${result.success ? "OK" : "FAILED"} ${serviceId}: ${result.message}`);
75657
+ } catch (error) {
75658
+ lines.push(
75659
+ `FAILED ${serviceId}: ${error instanceof Error ? error.message : String(error)}`
75660
+ );
75661
+ }
75662
+ }
75663
+ return {
75664
+ message: [
75665
+ `Restarted ${succeeded}/${RESTARTABLE_SERVICES2.length} project daemons.`,
75666
+ ...lines,
75667
+ "Governance skipped: read-only service."
75668
+ ].join("\n")
75669
+ };
75670
+ } finally {
75671
+ restarting = false;
75672
+ }
75673
+ }
75576
75674
  const panelRequest = trimmed === "" || trimmed === "open" || trimmed === "--open";
75577
75675
  if (!panelRequest) {
75578
- return { message: "Usage: /connections [open]" };
75676
+ return { message: "Usage: /connections [open|restart]" };
75579
75677
  }
75580
75678
  const opened = deps.onPanelOpen?.current?.("toggleConnectionsPanel") ?? false;
75581
75679
  if (opened) return { message: "" };
@@ -79586,6 +79684,50 @@ import {
79586
79684
  matchTodoIdFromPrompt
79587
79685
  } from "@wrongstack/tools/auto-proceed-loop-guard";
79588
79686
  import { todoTool } from "@wrongstack/tools/todo";
79687
+
79688
+ // src/hooks/failed-run-tracker.ts
79689
+ var FAILED_RUN_CONTINUE_PROMPT = "The previous run stopped on an API error (the provider failed after its retries), not on a decision of mine. Continue from where it stopped; do not redo work that is already done. If you cannot tell how far it got, say so and stop.";
79690
+ function createFailedRunTracker(events, leaderSessionId) {
79691
+ let providerFailure = null;
79692
+ let pendingFailure = false;
79693
+ const isLeader = (sessionId) => {
79694
+ const leader = leaderSessionId();
79695
+ return sessionId === void 0 || leader === void 0 || sessionId === leader;
79696
+ };
79697
+ const offs = [
79698
+ events.on("agent.run.started", (e) => {
79699
+ if (!isLeader(e.sessionId)) return;
79700
+ providerFailure = null;
79701
+ pendingFailure = false;
79702
+ }),
79703
+ events.on("provider.error", (e) => {
79704
+ if (!isLeader(e.sessionId)) return;
79705
+ providerFailure = { retryable: e.retryable };
79706
+ }),
79707
+ events.on("provider.response", (e) => {
79708
+ if (!isLeader(e.sessionId)) return;
79709
+ providerFailure = null;
79710
+ }),
79711
+ events.on("agent.run.completed", (e) => {
79712
+ if (!isLeader(e.sessionId)) return;
79713
+ pendingFailure = e.status === "failed" && providerFailure?.retryable === true;
79714
+ providerFailure = null;
79715
+ })
79716
+ ];
79717
+ return {
79718
+ pending: () => pendingFailure,
79719
+ consume() {
79720
+ const had = pendingFailure;
79721
+ pendingFailure = false;
79722
+ return had;
79723
+ },
79724
+ dispose() {
79725
+ for (const off of offs) off();
79726
+ }
79727
+ };
79728
+ }
79729
+
79730
+ // src/hooks/use-next-steps-auto-submit.ts
79589
79731
  async function commitTodoTransition(agent, todos) {
79590
79732
  if (!agent?.ctx?.state) return null;
79591
79733
  try {
@@ -79600,7 +79742,8 @@ function selectAutoProceedCandidate({
79600
79742
  suggestions = [],
79601
79743
  autoSuggestions = [],
79602
79744
  yolo = false,
79603
- autonomyNextPrompt
79745
+ autonomyNextPrompt,
79746
+ failedRun = false
79604
79747
  }) {
79605
79748
  const todo = resolveContinuation({ todos, suggestions: [] });
79606
79749
  if (todo.source === "todo") {
@@ -79612,7 +79755,12 @@ function selectAutoProceedCandidate({
79612
79755
  return { source: "auto-suggestion", prompt, label: automatic };
79613
79756
  }
79614
79757
  const suggestion = suggestions.find((item) => item.trim().length > 0)?.trim();
79615
- return suggestion ? { source: "suggestion", prompt: suggestion, label: suggestion } : null;
79758
+ if (suggestion) return { source: "suggestion", prompt: suggestion, label: suggestion };
79759
+ return failedRun ? {
79760
+ source: "failed-run",
79761
+ prompt: FAILED_RUN_CONTINUE_PROMPT,
79762
+ label: "resume after the API error"
79763
+ } : null;
79616
79764
  }
79617
79765
  function useNextStepsAutoSubmit({
79618
79766
  state,
@@ -79646,6 +79794,17 @@ function useNextStepsAutoSubmit({
79646
79794
  const autoSubmitLoopGuardRef = (0, import_react135.useRef)(createAutoProceedLoopGuard());
79647
79795
  const continuationAttemptTrackerRef = (0, import_react135.useRef)(createContinuationAttemptTracker());
79648
79796
  const [nextStepsRecheck, setNextStepsRecheck] = (0, import_react135.useState)(0);
79797
+ const failedRunTrackerRef = (0, import_react135.useRef)(null);
79798
+ (0, import_react135.useEffect)(() => {
79799
+ const events = agent?.events;
79800
+ if (!events) return;
79801
+ const tracker = createFailedRunTracker(events, () => agent?.ctx?.session?.id);
79802
+ failedRunTrackerRef.current = tracker;
79803
+ return () => {
79804
+ tracker.dispose();
79805
+ failedRunTrackerRef.current = null;
79806
+ };
79807
+ }, [agent]);
79649
79808
  (0, import_react135.useEffect)(() => {
79650
79809
  autoSubmitStreakRef.current = 0;
79651
79810
  autoSubmitCapWarnedRef.current = false;
@@ -79665,7 +79824,7 @@ function useNextStepsAutoSubmit({
79665
79824
  nextStepsAutoSubmitTodoIdRef.current = null;
79666
79825
  return;
79667
79826
  }
79668
- if (state.autoProceedHold) {
79827
+ if (state.autoProceedHold || state.steeringPending) {
79669
79828
  clearInterval(nextStepsAutoSubmitTimerRef.current);
79670
79829
  nextStepsAutoSubmitTimerRef.current = void 0;
79671
79830
  setNextStepsAutoSubmitCountdown(null);
@@ -79691,7 +79850,8 @@ function useNextStepsAutoSubmit({
79691
79850
  suggestions,
79692
79851
  autoSuggestions: isYolo ? getAutoSuggestions?.() ?? [] : [],
79693
79852
  yolo: isYolo,
79694
- autonomyNextPrompt
79853
+ autonomyNextPrompt,
79854
+ failedRun: failedRunTrackerRef.current?.pending() ?? false
79695
79855
  });
79696
79856
  nextStepsAutoSubmitTodosRef.current = allTodos;
79697
79857
  if (!candidate) {
@@ -79742,6 +79902,7 @@ function useNextStepsAutoSubmit({
79742
79902
  nextStepsAutoSubmitSuggestionRef.current = null;
79743
79903
  nextStepsAutoSubmitSourceRef.current = null;
79744
79904
  nextStepsAutoSubmitTodoIdRef.current = null;
79905
+ if (source === "failed-run") failedRunTrackerRef.current?.consume();
79745
79906
  if (suggestion) {
79746
79907
  if (source === "todo" && todoId) {
79747
79908
  if (agent?.ctx?.state) {
@@ -79835,7 +79996,7 @@ function useNextStepsAutoSubmit({
79835
79996
  }
79836
79997
  setSuggestions?.([]);
79837
79998
  const truncatedForMessage = suggestion.length > 100 ? `${suggestion.slice(0, 97)}\u2026` : suggestion;
79838
- const reason = source === "todo" ? stalledMatchFailed ? "the stalled prompt could not be matched to any todo on the board" : "No more open todos to advance to" : `the same prompt was fed ${repetition.runLength} times in a row`;
79999
+ const reason = source === "failed-run" ? "the run failed on an API error again after one automatic retry" : source === "todo" ? stalledMatchFailed ? "the stalled prompt could not be matched to any todo on the board" : "No more open todos to advance to" : `the same prompt was fed ${repetition.runLength} times in a row`;
79839
80000
  dispatch({
79840
80001
  type: "addEntry",
79841
80002
  entry: {
@@ -79879,6 +80040,7 @@ ${GROUNDED_NO_PROGRESS_STEER}` : suggestion;
79879
80040
  }, [
79880
80041
  state.status,
79881
80042
  state.autoProceedHold,
80043
+ state.steeringPending,
79882
80044
  autonomyLive,
79883
80045
  state.enhance,
79884
80046
  state.enhanceBusy,
@@ -80127,6 +80289,7 @@ function usePanelControllers({
80127
80289
  featureMemory: s2.featureMemory ?? true,
80128
80290
  featureSkills: s2.featureSkills ?? true,
80129
80291
  featureModelsRegistry: s2.featureModelsRegistry ?? true,
80292
+ featureToolCoach: s2.featureToolCoach ?? true,
80130
80293
  tokenSavingTier: s2.featureTokenSaving ?? "off",
80131
80294
  allowOutsideProjectRoot: s2.allowOutsideProjectRoot ?? true,
80132
80295
  contextAutoCompact: s2.contextAutoCompact ?? true,
@@ -82164,36 +82327,51 @@ function useThemePickerHandler({
82164
82327
  configStore,
82165
82328
  saveThemePreset,
82166
82329
  dispatch,
82167
- selectedIndex: selectedIndex2
82330
+ selectedIndex: selectedIndex2,
82331
+ filter,
82332
+ previous
82168
82333
  }) {
82334
+ const applyPreset = (0, import_react149.useCallback)(
82335
+ (presetName, verb) => {
82336
+ setActiveTheme(presetName);
82337
+ try {
82338
+ configStore?.update({ themePreset: presetName });
82339
+ void saveThemePreset?.(presetName).catch((err) => {
82340
+ dispatch({
82341
+ type: "addEntry",
82342
+ entry: {
82343
+ kind: "warn",
82344
+ text: `Theme applied in-memory but could not persist to disk: ${err instanceof Error ? err.message : String(err)}`
82345
+ }
82346
+ });
82347
+ });
82348
+ } catch {
82349
+ }
82350
+ dispatch({
82351
+ type: "addEntry",
82352
+ entry: {
82353
+ kind: "info",
82354
+ text: `Theme preset ${verb} "${presetName}"${getActiveThemeName() === presetName ? "" : ` (fallback applied: ${getActiveThemeName()})`}.`
82355
+ }
82356
+ });
82357
+ },
82358
+ [configStore, dispatch, saveThemePreset]
82359
+ );
82169
82360
  const onThemePickerEnter = (0, import_react149.useCallback)(() => {
82170
- const preset = THEME_OPTIONS[Math.max(0, selectedIndex2)];
82361
+ const preset = filterThemeOptions(THEME_OPTIONS, filter ?? "")[Math.max(0, selectedIndex2)];
82171
82362
  const presetName = preset?.id;
82172
82363
  if (!presetName) return;
82173
- setActiveTheme(presetName);
82174
- try {
82175
- configStore?.update({ themePreset: presetName });
82176
- void saveThemePreset?.(presetName).catch((err) => {
82177
- dispatch({
82178
- type: "addEntry",
82179
- entry: {
82180
- kind: "warn",
82181
- text: `Theme applied in-memory but could not persist to disk: ${err instanceof Error ? err.message : String(err)}`
82182
- }
82183
- });
82184
- });
82185
- } catch {
82186
- }
82187
- dispatch({
82188
- type: "addEntry",
82189
- entry: {
82190
- kind: "info",
82191
- text: `Theme preset switched to "${presetName}"${getActiveThemeName() === presetName ? "" : ` (fallback applied: ${getActiveThemeName()})`}.`
82192
- }
82193
- });
82364
+ applyPreset(presetName, "switched to");
82194
82365
  dispatch({ type: "themePickerClose" });
82195
- }, [configStore, dispatch, saveThemePreset, selectedIndex2]);
82196
- return { onThemePickerEnter };
82366
+ }, [applyPreset, dispatch, filter, selectedIndex2]);
82367
+ const onThemePickerUndo = (0, import_react149.useCallback)(() => {
82368
+ if (previous === void 0 || previous === getActiveThemeName()) return;
82369
+ const current = getActiveThemeName();
82370
+ applyPreset(previous, "restored to");
82371
+ dispatch({ type: "themePickerHint", text: `Undid \u2192 ${previous} (was ${current})` });
82372
+ dispatch({ type: "themePickerSwapPrevious" });
82373
+ }, [applyPreset, dispatch, previous]);
82374
+ return { onThemePickerEnter, onThemePickerUndo };
82197
82375
  }
82198
82376
 
82199
82377
  // src/hooks/use-tui-controllers.ts
@@ -83598,7 +83776,8 @@ function useSettingsSlashCommands(deps, part) {
83598
83776
  state,
83599
83777
  hiddenItemsRef,
83600
83778
  setHiddenItems,
83601
- openStatuslinePicker
83779
+ openStatuslinePicker,
83780
+ applyThemePreset
83602
83781
  } = deps;
83603
83782
  (0, import_react158.useEffect)(() => {
83604
83783
  if (part !== "core") return;
@@ -83870,10 +84049,11 @@ Available chords:
83870
84049
  message: `Unknown theme preset "${arg}". Available: ${names}`
83871
84050
  };
83872
84051
  }
83873
- dispatch({
83874
- type: "themePickerOpen",
83875
- selected: presetIdx
83876
- });
84052
+ if (applyThemePreset) {
84053
+ applyThemePreset(THEME_OPTIONS[presetIdx].id);
84054
+ return { message: void 0 };
84055
+ }
84056
+ dispatch({ type: "themePickerOpen", selected: presetIdx });
83877
84057
  return { message: void 0 };
83878
84058
  }
83879
84059
  dispatch({ type: "themePickerOpen" });
@@ -83909,7 +84089,10 @@ function useTuiSlashCommands({
83909
84089
  setMailboxPanelOpen,
83910
84090
  switchAutonomy,
83911
84091
  listSessions,
83912
- openPromptPicker
84092
+ openPromptPicker,
84093
+ configStore,
84094
+ saveThemePreset,
84095
+ applyThemePreset
83913
84096
  }) {
83914
84097
  const sliceDeps = {
83915
84098
  slashRegistry,
@@ -83932,7 +84115,10 @@ function useTuiSlashCommands({
83932
84115
  setMailboxPanelOpen,
83933
84116
  switchAutonomy,
83934
84117
  listSessions,
83935
- openPromptPicker
84118
+ openPromptPicker,
84119
+ configStore,
84120
+ saveThemePreset,
84121
+ applyThemePreset
83936
84122
  };
83937
84123
  useSessionSlashCommands(sliceDeps, "head");
83938
84124
  useProviderSlashCommands(sliceDeps);
@@ -84271,6 +84457,7 @@ function routeSettingsOverlayKey(host, input, key2, isEnter) {
84271
84457
  featureMemory: config.featureMemory ?? true,
84272
84458
  featureSkills: config.featureSkills ?? true,
84273
84459
  featureModelsRegistry: config.featureModelsRegistry ?? true,
84460
+ featureToolCoach: config.featureToolCoach ?? true,
84274
84461
  tokenSavingTier: config.featureTokenSaving ?? "off",
84275
84462
  allowOutsideProjectRoot: config.allowOutsideProjectRoot ?? true,
84276
84463
  contextAutoCompact: config.contextAutoCompact ?? true,
@@ -87190,19 +87377,33 @@ function tryAuthModelPickerKeys(host, input, key2, isEnter, debouncedEnter2) {
87190
87377
  if (state.themePicker.open) {
87191
87378
  if (key2.ctrl || key2.meta) return true;
87192
87379
  if (key2.escape) {
87380
+ if (state.themePicker.preview) {
87381
+ if (state.themePicker.previous) setActiveTheme(state.themePicker.previous);
87382
+ dispatch({ type: "themePickerPreview", on: false });
87383
+ dispatch({ type: "themePickerClose" });
87384
+ return true;
87385
+ }
87386
+ if (state.themePicker.filtering || state.themePicker.filter) {
87387
+ dispatch({ type: "themePickerFilterMode", on: false });
87388
+ return true;
87389
+ }
87193
87390
  dispatch({ type: "themePickerClose" });
87194
87391
  return true;
87195
87392
  }
87196
- if (key2.mouse?.kind === "wheel") {
87197
- dispatch({ type: "themePickerMove", delta: key2.mouse.wheel > 0 ? -1 : 1 });
87198
- return true;
87199
- }
87200
- if (key2.upArrow) {
87201
- dispatch({ type: "themePickerMove", delta: -1 });
87393
+ if (!state.themePicker.filtering && input === "p") {
87394
+ dispatch({ type: "themePickerPreview", on: !state.themePicker.preview });
87202
87395
  return true;
87203
87396
  }
87204
- if (key2.downArrow) {
87205
- dispatch({ type: "themePickerMove", delta: 1 });
87397
+ const previewed = state.themePicker.preview;
87398
+ const moveBy = key2.mouse?.kind === "wheel" ? key2.mouse.wheel > 0 ? -1 : 1 : 0;
87399
+ if (key2.upArrow || key2.downArrow || moveBy !== 0) {
87400
+ const delta = key2.upArrow ? -1 : key2.downArrow ? 1 : moveBy;
87401
+ const next = (state.themePicker.selected + delta + filterThemeOptions(THEME_OPTIONS, state.themePicker.filter ?? "").length) % filterThemeOptions(THEME_OPTIONS, state.themePicker.filter ?? "").length;
87402
+ if (previewed) {
87403
+ const focused = filterThemeOptions(THEME_OPTIONS, state.themePicker.filter ?? "")[next];
87404
+ if (focused) setActiveTheme(focused.id);
87405
+ }
87406
+ dispatch({ type: "themePickerMove", delta });
87206
87407
  return true;
87207
87408
  }
87208
87409
  if (isEnter) {
@@ -87215,6 +87416,30 @@ function tryAuthModelPickerKeys(host, input, key2, isEnter, debouncedEnter2) {
87215
87416
  }
87216
87417
  return true;
87217
87418
  }
87419
+ const query = state.themePicker.filter ?? "";
87420
+ if (!key2.ctrl && input === "/") {
87421
+ dispatch({ type: "themePickerFilterMode", on: !state.themePicker.filtering });
87422
+ return true;
87423
+ }
87424
+ if (state.themePicker.filtering) {
87425
+ if (key2.delete || input === "\x7F" || key2.backspace) {
87426
+ dispatch({ type: "themePickerFilter", text: query.slice(0, -1) });
87427
+ return true;
87428
+ }
87429
+ if (input && !key2.ctrl && !key2.meta && input.length === 1 && input >= " ") {
87430
+ dispatch({ type: "themePickerFilter", text: query + input });
87431
+ return true;
87432
+ }
87433
+ }
87434
+ if (!state.themePicker.filtering && input === "u") {
87435
+ host.inputGateRef.current = true;
87436
+ try {
87437
+ host.onThemePickerUndo?.();
87438
+ } finally {
87439
+ host.inputGateRef.current = false;
87440
+ }
87441
+ return true;
87442
+ }
87218
87443
  return true;
87219
87444
  }
87220
87445
  if (state.skillPicker.open) {
@@ -88148,6 +88373,7 @@ function useAppPickerKeys({
88148
88373
  statuslineHiddenForPicker,
88149
88374
  onPickerEnter,
88150
88375
  onThemePickerEnter,
88376
+ onThemePickerUndo,
88151
88377
  setPromptFavorite
88152
88378
  }) {
88153
88379
  const {
@@ -88646,7 +88872,8 @@ function useAppPickerKeys({
88646
88872
  if (action) dispatch(action);
88647
88873
  },
88648
88874
  onPickerEnter,
88649
- onThemePickerEnter
88875
+ onThemePickerEnter,
88876
+ onThemePickerUndo
88650
88877
  });
88651
88878
  }
88652
88879
 
@@ -89187,6 +89414,7 @@ function useControllerKeyPipeline({
89187
89414
  statuslineHiddenForPicker,
89188
89415
  onPickerEnter,
89189
89416
  onThemePickerEnter,
89417
+ onThemePickerUndo,
89190
89418
  setPromptFavorite,
89191
89419
  stateRef,
89192
89420
  exitRequestedRef,
@@ -89306,6 +89534,7 @@ function useControllerKeyPipeline({
89306
89534
  statuslineHiddenForPicker,
89307
89535
  onPickerEnter,
89308
89536
  onThemePickerEnter,
89537
+ onThemePickerUndo,
89309
89538
  setPromptFavorite
89310
89539
  });
89311
89540
  const { interruptsSyncRef, runInterruptLadder } = useInterruptLadder({
@@ -89926,6 +90155,30 @@ function useAppController(props) {
89926
90155
  clearDraft,
89927
90156
  runBlocksRef
89928
90157
  });
90158
+ const applyThemePreset = (0, import_react163.useCallback)(
90159
+ (preset) => {
90160
+ setActiveTheme(preset);
90161
+ const warnPersist = (err) => dispatch({
90162
+ type: "addEntry",
90163
+ entry: {
90164
+ kind: "warn",
90165
+ text: `Theme applied in-memory but could not persist to disk: ${err instanceof Error ? err.message : String(err)}`
90166
+ }
90167
+ });
90168
+ try {
90169
+ configStore?.update({ themePreset: preset });
90170
+ } catch (err) {
90171
+ warnPersist(err);
90172
+ return;
90173
+ }
90174
+ try {
90175
+ void props.saveThemePreset?.(preset).catch(warnPersist);
90176
+ } catch (err) {
90177
+ warnPersist(err);
90178
+ }
90179
+ },
90180
+ [configStore, dispatch, props.saveThemePreset]
90181
+ );
89929
90182
  useTuiSlashCommands({
89930
90183
  slashRegistry,
89931
90184
  skillLoader: props.skillLoader,
@@ -89947,7 +90200,9 @@ function useAppController(props) {
89947
90200
  setMailboxPanelOpen,
89948
90201
  switchAutonomy,
89949
90202
  listSessions,
89950
- openPromptPicker
90203
+ openPromptPicker,
90204
+ configStore,
90205
+ applyThemePreset
89951
90206
  });
89952
90207
  useProviderEventBridge({
89953
90208
  events,
@@ -90067,11 +90322,13 @@ function useAppController(props) {
90067
90322
  setDraft,
90068
90323
  tokenPreviewsRef
90069
90324
  });
90070
- const { onThemePickerEnter } = useThemePickerHandler({
90325
+ const { onThemePickerEnter, onThemePickerUndo } = useThemePickerHandler({
90071
90326
  configStore,
90072
90327
  saveThemePreset: props.saveThemePreset,
90073
90328
  dispatch,
90074
- selectedIndex: state.themePicker.selected
90329
+ selectedIndex: state.themePicker.selected,
90330
+ filter: state.themePicker.filter,
90331
+ previous: state.themePicker.previous
90075
90332
  });
90076
90333
  const { runInterruptLadder, stableOnKey } = useControllerKeyPipeline({
90077
90334
  sessionGenerationRef,
@@ -90098,6 +90355,7 @@ function useAppController(props) {
90098
90355
  statuslineHiddenForPicker,
90099
90356
  onPickerEnter,
90100
90357
  onThemePickerEnter,
90358
+ onThemePickerUndo,
90101
90359
  setPromptFavorite,
90102
90360
  stateRef,
90103
90361
  exitRequestedRef,