@rk0429/agentic-relay 1.3.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/relay.mjs +1357 -14
  2. package/package.json +11 -1
package/dist/relay.mjs CHANGED
@@ -1027,7 +1027,7 @@ function buildContextFromEnv() {
1027
1027
  const depth = Number(process.env["RELAY_DEPTH"] ?? "0");
1028
1028
  return { traceId, parentSessionId, depth };
1029
1029
  }
1030
- async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress, agentEventStore, hookMemoryDir = "./memory") {
1030
+ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress, agentEventStore, hookMemoryDir = "./memory", taskCompleter) {
1031
1031
  onProgress?.({ stage: "initializing", percent: 0 });
1032
1032
  let effectiveBackend;
1033
1033
  let selectionReason;
@@ -1378,6 +1378,20 @@ ${input.prompt}`;
1378
1378
  tokenUsage: result.tokenUsage
1379
1379
  }
1380
1380
  });
1381
+ if (taskCompleter && session.metadata.taskId) {
1382
+ try {
1383
+ await taskCompleter.completeTask(
1384
+ session.metadata.taskId,
1385
+ session.metadata.agentType ?? session.relaySessionId,
1386
+ `Auto-completed by spawn_agent. Session: ${session.relaySessionId}`
1387
+ );
1388
+ logger.info(`Auto-completed task ${session.metadata.taskId} via taskCompleter`);
1389
+ } catch (taskError) {
1390
+ logger.warn(
1391
+ `Failed to auto-complete task ${session.metadata.taskId}: ${taskError instanceof Error ? taskError.message : String(taskError)}`
1392
+ );
1393
+ }
1394
+ }
1381
1395
  if (hooksEngine2) {
1382
1396
  try {
1383
1397
  await hooksEngine2.emit("on-session-complete", {
@@ -1717,7 +1731,7 @@ var init_conflict_detector = __esm({
1717
1731
  });
1718
1732
 
1719
1733
  // src/mcp-server/tools/spawn-agents-parallel.ts
1720
- async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress, agentEventStore, hookMemoryDir = "./memory") {
1734
+ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress, agentEventStore, hookMemoryDir = "./memory", taskCompleter) {
1721
1735
  for (let index = 0; index < agents.length; index += 1) {
1722
1736
  try {
1723
1737
  resolveValidatedSessionMetadata(agents[index]);
@@ -1800,7 +1814,8 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
1800
1814
  childHttpUrl,
1801
1815
  void 0,
1802
1816
  agentEventStore,
1803
- hookMemoryDir
1817
+ hookMemoryDir,
1818
+ taskCompleter
1804
1819
  ).then((result) => {
1805
1820
  completedCount++;
1806
1821
  onProgress?.({
@@ -2324,7 +2339,7 @@ var init_server = __esm({
2324
2339
  };
2325
2340
  MAX_CHILD_HTTP_SESSIONS = 100;
2326
2341
  RelayMCPServer = class {
2327
- constructor(registry2, sessionManager2, guardConfig, hooksEngine2, contextMonitor2, inlineSummaryLength, responseOutputDir, relayConfig) {
2342
+ constructor(registry2, sessionManager2, guardConfig, hooksEngine2, contextMonitor2, inlineSummaryLength, responseOutputDir, relayConfig, taskCompleter) {
2328
2343
  this.registry = registry2;
2329
2344
  this.sessionManager = sessionManager2;
2330
2345
  this.hooksEngine = hooksEngine2;
@@ -2332,6 +2347,7 @@ var init_server = __esm({
2332
2347
  this.inlineSummaryLength = inlineSummaryLength;
2333
2348
  this.responseOutputDir = responseOutputDir;
2334
2349
  this.relayConfig = relayConfig;
2350
+ this.taskCompleter = taskCompleter;
2335
2351
  this.guard = new RecursionGuard(guardConfig);
2336
2352
  this.backendSelector = new BackendSelector();
2337
2353
  this.staleThresholdSec = relayConfig?.sessionHealth?.staleThresholdSec ?? 300;
@@ -2362,7 +2378,7 @@ var init_server = __esm({
2362
2378
  this.agentEventStore
2363
2379
  );
2364
2380
  this.server = new McpServer(
2365
- { name: "agentic-relay", version: "1.3.1" },
2381
+ { name: "agentic-relay", version: "1.4.1" },
2366
2382
  createMcpServerOptions()
2367
2383
  );
2368
2384
  this.registerTools(this.server);
@@ -2403,7 +2419,8 @@ var init_server = __esm({
2403
2419
  this._childHttpUrl,
2404
2420
  void 0,
2405
2421
  this.agentEventStore,
2406
- this.hookMemoryDir
2422
+ this.hookMemoryDir,
2423
+ this.taskCompleter
2407
2424
  );
2408
2425
  const controlOptions = {
2409
2426
  inlineSummaryLength: this.inlineSummaryLength ?? DEFAULT_INLINE_SUMMARY_LENGTH,
@@ -2458,7 +2475,8 @@ var init_server = __esm({
2458
2475
  this._childHttpUrl,
2459
2476
  void 0,
2460
2477
  this.agentEventStore,
2461
- this.hookMemoryDir
2478
+ this.hookMemoryDir,
2479
+ this.taskCompleter
2462
2480
  );
2463
2481
  const controlOptions = {
2464
2482
  inlineSummaryLength: this.inlineSummaryLength ?? DEFAULT_INLINE_SUMMARY_LENGTH,
@@ -2579,7 +2597,8 @@ var init_server = __esm({
2579
2597
  this._childHttpUrl,
2580
2598
  void 0,
2581
2599
  this.agentEventStore,
2582
- this.hookMemoryDir
2600
+ this.hookMemoryDir,
2601
+ this.taskCompleter
2583
2602
  );
2584
2603
  const controlOptions = {
2585
2604
  inlineSummaryLength: this.inlineSummaryLength ?? DEFAULT_INLINE_SUMMARY_LENGTH,
@@ -2830,7 +2849,7 @@ var init_server = __esm({
2830
2849
  sessionIdGenerator: () => randomUUID()
2831
2850
  });
2832
2851
  const server = new McpServer(
2833
- { name: "agentic-relay", version: "1.3.1" },
2852
+ { name: "agentic-relay", version: "1.4.1" },
2834
2853
  createMcpServerOptions()
2835
2854
  );
2836
2855
  this.registerTools(server);
@@ -2895,8 +2914,1249 @@ var init_server = __esm({
2895
2914
  }
2896
2915
  });
2897
2916
 
2917
+ // src/tui/services/conversation-engine.ts
2918
+ import { nanoid as nanoid4 } from "nanoid";
2919
+ var ConversationEngine;
2920
+ var init_conversation_engine = __esm({
2921
+ "src/tui/services/conversation-engine.ts"() {
2922
+ "use strict";
2923
+ ConversationEngine = class {
2924
+ constructor(registry2, sessionManager2, hooksEngine2, contextMonitor2) {
2925
+ this.registry = registry2;
2926
+ this.sessionManager = sessionManager2;
2927
+ this.hooksEngine = hooksEngine2;
2928
+ this.contextMonitor = contextMonitor2;
2929
+ }
2930
+ messages = [];
2931
+ sessionId = null;
2932
+ totalTokens = { input: 0, output: 0 };
2933
+ /** セッション初期化。session-init hook を発火 */
2934
+ async initSession(backendId) {
2935
+ const session = await this.sessionManager.create({ backendId });
2936
+ this.sessionId = session.relaySessionId;
2937
+ await this.hooksEngine.emit("session-init", {
2938
+ event: "session-init",
2939
+ sessionId: session.relaySessionId,
2940
+ backendId,
2941
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2942
+ data: {}
2943
+ });
2944
+ return session.relaySessionId;
2945
+ }
2946
+ /** ユーザー入力を処理し、ConversationEvent を生成する AsyncGenerator */
2947
+ async *processInput(text, backendId) {
2948
+ const { results: prePromptResults } = await this.hooksEngine.emitAndCollectMetadata("pre-prompt", {
2949
+ event: "pre-prompt",
2950
+ sessionId: this.sessionId ?? "",
2951
+ backendId,
2952
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2953
+ data: { prompt: text }
2954
+ });
2955
+ const rejected = prePromptResults.find((r) => !r.output.allow);
2956
+ if (rejected) {
2957
+ yield {
2958
+ type: "error",
2959
+ message: rejected.output.message || "Request rejected by pre-prompt hook"
2960
+ };
2961
+ return;
2962
+ }
2963
+ this.messages.push({
2964
+ id: nanoid4(),
2965
+ role: "user",
2966
+ content: text,
2967
+ timestamp: /* @__PURE__ */ new Date(),
2968
+ backendId
2969
+ });
2970
+ const adapter = this.registry.get(backendId);
2971
+ const flags = {
2972
+ prompt: text,
2973
+ outputFormat: "stream-json"
2974
+ };
2975
+ if (this.sessionId) {
2976
+ const session = await this.sessionManager.get(this.sessionId);
2977
+ if (session?.nativeSessionId) {
2978
+ flags.resume = session.nativeSessionId;
2979
+ }
2980
+ }
2981
+ let assistantContent = "";
2982
+ if (adapter.executeStreaming) {
2983
+ for await (const event of adapter.executeStreaming(flags)) {
2984
+ yield this.convertStreamEvent(event);
2985
+ if (event.type === "text") {
2986
+ assistantContent += event.text;
2987
+ }
2988
+ if (event.type === "done") {
2989
+ await this.handleDoneEvent(event, backendId);
2990
+ }
2991
+ }
2992
+ } else {
2993
+ const result = await adapter.execute(flags);
2994
+ assistantContent = result.stdout;
2995
+ yield { type: "text", text: result.stdout };
2996
+ if (result.nativeSessionId && this.sessionId) {
2997
+ await this.sessionManager.update(this.sessionId, {
2998
+ nativeSessionId: result.nativeSessionId
2999
+ });
3000
+ }
3001
+ if (result.tokenUsage) {
3002
+ this.totalTokens.input += result.tokenUsage.inputTokens;
3003
+ this.totalTokens.output += result.tokenUsage.outputTokens;
3004
+ this.contextMonitor.updateUsage(
3005
+ this.sessionId ?? "",
3006
+ backendId,
3007
+ this.totalTokens.input + this.totalTokens.output
3008
+ );
3009
+ }
3010
+ yield { type: "done", result };
3011
+ }
3012
+ this.messages.push({
3013
+ id: nanoid4(),
3014
+ role: "assistant",
3015
+ content: assistantContent,
3016
+ timestamp: /* @__PURE__ */ new Date(),
3017
+ backendId
3018
+ });
3019
+ await this.hooksEngine.emit("post-response", {
3020
+ event: "post-response",
3021
+ sessionId: this.sessionId ?? "",
3022
+ backendId,
3023
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3024
+ data: { response: assistantContent }
3025
+ });
3026
+ }
3027
+ /** StreamEvent → ConversationEvent の変換ヘルパー */
3028
+ convertStreamEvent(event) {
3029
+ if (event.type === "done") {
3030
+ return { type: "done", result: event.result };
3031
+ }
3032
+ return event;
3033
+ }
3034
+ /** done イベントのハンドリング: セッション更新 + トークン累積 + ContextMonitor 更新 */
3035
+ async handleDoneEvent(event, backendId) {
3036
+ if (event.nativeSessionId && this.sessionId) {
3037
+ await this.sessionManager.update(this.sessionId, {
3038
+ nativeSessionId: event.nativeSessionId
3039
+ });
3040
+ }
3041
+ if (event.result.tokenUsage) {
3042
+ this.totalTokens.input += event.result.tokenUsage.inputTokens;
3043
+ this.totalTokens.output += event.result.tokenUsage.outputTokens;
3044
+ this.contextMonitor.updateUsage(
3045
+ this.sessionId ?? "",
3046
+ backendId,
3047
+ this.totalTokens.input + this.totalTokens.output
3048
+ );
3049
+ }
3050
+ }
3051
+ getMessages() {
3052
+ return [...this.messages];
3053
+ }
3054
+ getSessionId() {
3055
+ return this.sessionId;
3056
+ }
3057
+ getTotalTokens() {
3058
+ return { ...this.totalTokens };
3059
+ }
3060
+ /** バックエンドを切り替え、新セッションを初期化 */
3061
+ async switchBackend(newBackendId, contextSummary, _resumeSessionId) {
3062
+ if (this.sessionId) {
3063
+ await this.sessionManager.update(this.sessionId, {
3064
+ status: "completed"
3065
+ });
3066
+ }
3067
+ const session = await this.sessionManager.create({
3068
+ backendId: newBackendId
3069
+ });
3070
+ this.sessionId = session.relaySessionId;
3071
+ await this.hooksEngine.emit("session-init", {
3072
+ event: "session-init",
3073
+ sessionId: session.relaySessionId,
3074
+ backendId: newBackendId,
3075
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3076
+ data: { contextSummary }
3077
+ });
3078
+ if (contextSummary) {
3079
+ this.messages.push({
3080
+ id: nanoid4(),
3081
+ role: "system",
3082
+ content: contextSummary,
3083
+ timestamp: /* @__PURE__ */ new Date(),
3084
+ backendId: newBackendId
3085
+ });
3086
+ }
3087
+ this.totalTokens = { input: 0, output: 0 };
3088
+ return session.relaySessionId;
3089
+ }
3090
+ clearMessages() {
3091
+ this.messages = [];
3092
+ }
3093
+ };
3094
+ }
3095
+ });
3096
+
3097
+ // src/tui/services/backend-orchestrator.ts
3098
+ var BackendOrchestrator;
3099
+ var init_backend_orchestrator = __esm({
3100
+ "src/tui/services/backend-orchestrator.ts"() {
3101
+ "use strict";
3102
+ BackendOrchestrator = class {
3103
+ constructor(registry2, sessionManager2, hooksEngine2, strategy = "shared-history") {
3104
+ this.registry = registry2;
3105
+ this.sessionManager = sessionManager2;
3106
+ this.hooksEngine = hooksEngine2;
3107
+ this.strategy = strategy;
3108
+ }
3109
+ backends = /* @__PURE__ */ new Map();
3110
+ activeBackendId = null;
3111
+ /** 各バックエンドの relay sessionId を保持(suspend/resume 用) */
3112
+ sessionIds = /* @__PURE__ */ new Map();
3113
+ /** バックエンドを検出し初期化。優先順位: claude > codex > gemini */
3114
+ async initialize(preferredBackend) {
3115
+ const priorities = ["claude", "codex", "gemini"];
3116
+ for (const id of priorities) {
3117
+ if (!this.registry.has(id)) continue;
3118
+ const adapter = this.registry.get(id);
3119
+ const health = await adapter.checkHealth();
3120
+ this.backends.set(id, {
3121
+ id,
3122
+ status: health.healthy ? "available" : "unavailable",
3123
+ health,
3124
+ nativeSessionId: null
3125
+ });
3126
+ }
3127
+ if (preferredBackend && this.backends.get(preferredBackend)?.status === "available") {
3128
+ this.activeBackendId = preferredBackend;
3129
+ this.backends.get(preferredBackend).status = "active";
3130
+ return preferredBackend;
3131
+ }
3132
+ const defaultBackend = priorities.find(
3133
+ (id) => this.backends.get(id)?.status === "available"
3134
+ );
3135
+ if (!defaultBackend) {
3136
+ throw new Error("No available backends found");
3137
+ }
3138
+ this.activeBackendId = defaultBackend;
3139
+ this.backends.get(defaultBackend).status = "active";
3140
+ return defaultBackend;
3141
+ }
3142
+ /** バックエンド切り替え(切替戦略・hook 発火・resume 対応) */
3143
+ async switchTo(target, messages) {
3144
+ const state = this.backends.get(target);
3145
+ if (!state || state.status === "unavailable") {
3146
+ throw new Error(`Backend ${target} is not available`);
3147
+ }
3148
+ const from = this.activeBackendId;
3149
+ if (from) {
3150
+ await this.hooksEngine.emit("pre-prompt", {
3151
+ event: "pre-prompt",
3152
+ sessionId: this.sessionIds.get(from) ?? "",
3153
+ backendId: from,
3154
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3155
+ data: { type: "backend-switch", from, to: target }
3156
+ });
3157
+ const prev = this.backends.get(from);
3158
+ if (prev) prev.status = "available";
3159
+ }
3160
+ this.activeBackendId = target;
3161
+ state.status = "active";
3162
+ const result = {};
3163
+ if (this.strategy === "shared-history" && messages && messages.length > 0) {
3164
+ result.contextSummary = this.generateContextSummary(messages);
3165
+ }
3166
+ const existingSessionId = this.sessionIds.get(target);
3167
+ if (existingSessionId) {
3168
+ result.resumeSessionId = existingSessionId;
3169
+ }
3170
+ return result;
3171
+ }
3172
+ /** メッセージ履歴からコンテキスト要約を生成 */
3173
+ generateContextSummary(messages) {
3174
+ const maxChars = 2e3;
3175
+ const relevant = messages.slice(-20);
3176
+ const lines = [];
3177
+ lines.push("=== Previous conversation context ===");
3178
+ for (const msg of relevant) {
3179
+ const prefix = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
3180
+ const truncated = msg.content.length > 200 ? msg.content.slice(0, 200) + "..." : msg.content;
3181
+ lines.push(`${prefix}: ${truncated}`);
3182
+ }
3183
+ lines.push("=== End of context ===");
3184
+ const summary = lines.join("\n");
3185
+ return summary.length > maxChars ? summary.slice(0, maxChars) + "\n..." : summary;
3186
+ }
3187
+ /** セッション ID を登録 */
3188
+ setSessionId(backendId, sessionId) {
3189
+ this.sessionIds.set(backendId, sessionId);
3190
+ }
3191
+ /** セッション ID を取得 */
3192
+ getSessionId(backendId) {
3193
+ return this.sessionIds.get(backendId);
3194
+ }
3195
+ /** 現在の切替戦略を取得 */
3196
+ getStrategy() {
3197
+ return this.strategy;
3198
+ }
3199
+ /** 切替戦略を変更 */
3200
+ setStrategy(strategy) {
3201
+ this.strategy = strategy;
3202
+ }
3203
+ getActiveId() {
3204
+ return this.activeBackendId;
3205
+ }
3206
+ getActiveAdapter() {
3207
+ if (!this.activeBackendId) {
3208
+ throw new Error("No active backend");
3209
+ }
3210
+ return this.registry.get(this.activeBackendId);
3211
+ }
3212
+ getStatuses() {
3213
+ return [...this.backends.values()];
3214
+ }
3215
+ isAvailable(id) {
3216
+ const state = this.backends.get(id);
3217
+ return state?.status === "available" || state?.status === "active";
3218
+ }
3219
+ };
3220
+ }
3221
+ });
3222
+
3223
+ // src/tui/components/StatusBar.tsx
3224
+ import { Box, Text } from "ink";
3225
+ import Spinner from "ink-spinner";
3226
+ import { jsx, jsxs } from "react/jsx-runtime";
3227
+ function StatusBar({ backendId, model, sessionId, tokenCount, isStreaming, showTokenCount }) {
3228
+ const totalTokens = tokenCount.input + tokenCount.output;
3229
+ const tokenStr = totalTokens > 1e3 ? `${(totalTokens / 1e3).toFixed(1)}k` : `${totalTokens}`;
3230
+ return /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderBottom: true, paddingX: 1, children: [
3231
+ /* @__PURE__ */ jsx(Box, { marginRight: 2, children: /* @__PURE__ */ jsx(Text, { bold: true, color: BACKEND_COLORS[backendId], children: backendId }) }),
3232
+ model && /* @__PURE__ */ jsx(Box, { marginRight: 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: model }) }),
3233
+ showTokenCount !== false && /* @__PURE__ */ jsx(Box, { marginRight: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
3234
+ tokenStr,
3235
+ " tokens"
3236
+ ] }) }),
3237
+ sessionId && /* @__PURE__ */ jsx(Box, { marginRight: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
3238
+ "session: ",
3239
+ sessionId.slice(0, 8)
3240
+ ] }) }),
3241
+ isStreaming && /* @__PURE__ */ jsxs(Box, { children: [
3242
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
3243
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " streaming" })
3244
+ ] })
3245
+ ] });
3246
+ }
3247
+ var BACKEND_COLORS;
3248
+ var init_StatusBar = __esm({
3249
+ "src/tui/components/StatusBar.tsx"() {
3250
+ "use strict";
3251
+ BACKEND_COLORS = {
3252
+ claude: "#D97706",
3253
+ // amber
3254
+ codex: "#059669",
3255
+ // emerald
3256
+ gemini: "#2563EB"
3257
+ // blue
3258
+ };
3259
+ }
3260
+ });
3261
+
3262
+ // src/tui/services/markdown-renderer.ts
3263
+ import { marked } from "marked";
3264
+ import { markedTerminal } from "marked-terminal";
3265
+ var MarkdownRenderer;
3266
+ var init_markdown_renderer = __esm({
3267
+ "src/tui/services/markdown-renderer.ts"() {
3268
+ "use strict";
3269
+ MarkdownRenderer = class {
3270
+ constructor() {
3271
+ marked.use(markedTerminal());
3272
+ }
3273
+ render(markdown) {
3274
+ return marked.parse(markdown);
3275
+ }
3276
+ };
3277
+ }
3278
+ });
3279
+
3280
+ // src/tui/components/MarkdownBlock.tsx
3281
+ import { Text as Text2 } from "ink";
3282
+ import { jsx as jsx2 } from "react/jsx-runtime";
3283
+ function MarkdownBlock({ content }) {
3284
+ const rendered = renderer.render(content);
3285
+ return /* @__PURE__ */ jsx2(Text2, { children: rendered });
3286
+ }
3287
+ var renderer;
3288
+ var init_MarkdownBlock = __esm({
3289
+ "src/tui/components/MarkdownBlock.tsx"() {
3290
+ "use strict";
3291
+ init_markdown_renderer();
3292
+ renderer = new MarkdownRenderer();
3293
+ }
3294
+ });
3295
+
3296
+ // src/tui/components/ChatView.tsx
3297
+ import { Box as Box2, Text as Text3, Static } from "ink";
3298
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
3299
+ function ChatView({ messages, streamingText, isStreaming }) {
3300
+ const completedMessages = isStreaming ? messages.slice(0, -1) : messages;
3301
+ const lastMessage = isStreaming ? null : messages[messages.length - 1];
3302
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 1, children: [
3303
+ /* @__PURE__ */ jsx3(Static, { items: completedMessages, children: (msg) => /* @__PURE__ */ jsx3(MessageLine, { message: msg }, msg.id) }),
3304
+ lastMessage && /* @__PURE__ */ jsx3(MessageLine, { message: lastMessage }),
3305
+ isStreaming && streamingText && /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(MarkdownBlock, { content: streamingText }) })
3306
+ ] });
3307
+ }
3308
+ function MessageLine({ message }) {
3309
+ if (message.role === "user") {
3310
+ return /* @__PURE__ */ jsxs2(Box2, { marginY: 0, children: [
3311
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: ">>> " }),
3312
+ /* @__PURE__ */ jsx3(Text3, { children: message.content })
3313
+ ] });
3314
+ }
3315
+ if (message.role === "system") {
3316
+ return /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: message.content }) });
3317
+ }
3318
+ return /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(MarkdownBlock, { content: message.content }) });
3319
+ }
3320
+ var init_ChatView = __esm({
3321
+ "src/tui/components/ChatView.tsx"() {
3322
+ "use strict";
3323
+ init_MarkdownBlock();
3324
+ }
3325
+ });
3326
+
3327
+ // src/tui/components/InputArea.tsx
3328
+ import { Box as Box3, Text as Text4 } from "ink";
3329
+ import TextInput from "ink-text-input";
3330
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
3331
+ function InputArea({ value, onChange, onSubmit, isDisabled, placeholder }) {
3332
+ return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "single", borderTop: true, paddingX: 1, children: [
3333
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "green", children: "> " }),
3334
+ isDisabled ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "waiting for response..." }) : /* @__PURE__ */ jsx4(
3335
+ TextInput,
3336
+ {
3337
+ value,
3338
+ onChange,
3339
+ onSubmit,
3340
+ placeholder: placeholder ?? "Type a message..."
3341
+ }
3342
+ )
3343
+ ] });
3344
+ }
3345
+ var init_InputArea = __esm({
3346
+ "src/tui/components/InputArea.tsx"() {
3347
+ "use strict";
3348
+ }
3349
+ });
3350
+
3351
+ // src/tui/components/ToolProgress.tsx
3352
+ import { Box as Box4, Text as Text5 } from "ink";
3353
+ import Spinner2 from "ink-spinner";
3354
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3355
+ function ToolProgress({ tools }) {
3356
+ if (tools.length === 0) return /* @__PURE__ */ jsx5(Fragment, {});
3357
+ return /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", marginLeft: 2, children: tools.map((tool) => /* @__PURE__ */ jsxs4(Box4, { children: [
3358
+ tool.status === "running" && /* @__PURE__ */ jsxs4(Text5, { color: "yellow", children: [
3359
+ /* @__PURE__ */ jsx5(Spinner2, { type: "dots" }),
3360
+ " "
3361
+ ] }),
3362
+ tool.status === "completed" && /* @__PURE__ */ jsx5(Text5, { color: "green", children: "[ok] " }),
3363
+ tool.status === "failed" && /* @__PURE__ */ jsx5(Text5, { color: "red", children: "[err] " }),
3364
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: tool.tool })
3365
+ ] }, tool.id)) });
3366
+ }
3367
+ var init_ToolProgress = __esm({
3368
+ "src/tui/components/ToolProgress.tsx"() {
3369
+ "use strict";
3370
+ }
3371
+ });
3372
+
3373
+ // src/tui/components/BackendIndicator.tsx
3374
+ import { Box as Box5, Text as Text6 } from "ink";
3375
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3376
+ function BackendIndicator({ statuses, activeBackendId }) {
3377
+ const order = ["claude", "codex", "gemini"];
3378
+ return /* @__PURE__ */ jsx6(Box5, { paddingX: 1, gap: 2, children: order.map((id) => {
3379
+ const state = statuses.find((s) => s.id === id);
3380
+ if (!state) return null;
3381
+ const label = BACKEND_LABELS[id];
3382
+ const isActive = id === activeBackendId;
3383
+ const isUnavailable = state.status === "unavailable";
3384
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
3385
+ /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
3386
+ label.key,
3387
+ " "
3388
+ ] }),
3389
+ isUnavailable ? /* @__PURE__ */ jsx6(Text6, { strikethrough: true, dimColor: true, children: label.name }) : isActive ? /* @__PURE__ */ jsx6(Text6, { bold: true, color: BACKEND_COLORS2[id], children: label.name }) : /* @__PURE__ */ jsx6(Text6, { color: BACKEND_COLORS2[id], children: label.name })
3390
+ ] }, id);
3391
+ }) });
3392
+ }
3393
+ var BACKEND_LABELS, BACKEND_COLORS2;
3394
+ var init_BackendIndicator = __esm({
3395
+ "src/tui/components/BackendIndicator.tsx"() {
3396
+ "use strict";
3397
+ BACKEND_LABELS = {
3398
+ claude: { key: "^1", name: "Claude" },
3399
+ codex: { key: "^2", name: "Codex" },
3400
+ gemini: { key: "^3", name: "Gemini" }
3401
+ };
3402
+ BACKEND_COLORS2 = {
3403
+ claude: "#D97706",
3404
+ codex: "#059669",
3405
+ gemini: "#2563EB"
3406
+ };
3407
+ }
3408
+ });
3409
+
3410
+ // src/tui/services/task-poller.ts
3411
+ var TaskPoller;
3412
+ var init_task_poller = __esm({
3413
+ "src/tui/services/task-poller.ts"() {
3414
+ "use strict";
3415
+ TaskPoller = class {
3416
+ constructor(eventStore, parentSessionId) {
3417
+ this.eventStore = eventStore;
3418
+ this.parentSessionId = parentSessionId;
3419
+ }
3420
+ tasks = /* @__PURE__ */ new Map();
3421
+ lastEventId = null;
3422
+ pollTimer = null;
3423
+ onUpdate = null;
3424
+ /** コールバック登録 */
3425
+ setOnUpdate(callback) {
3426
+ this.onUpdate = callback;
3427
+ }
3428
+ /** ポーリング開始 */
3429
+ startPolling(intervalMs = 2e3) {
3430
+ if (this.pollTimer) return;
3431
+ void this.poll();
3432
+ this.pollTimer = setInterval(() => void this.poll(), intervalMs);
3433
+ }
3434
+ /** ポーリング停止 */
3435
+ stopPolling() {
3436
+ if (this.pollTimer) {
3437
+ clearInterval(this.pollTimer);
3438
+ this.pollTimer = null;
3439
+ }
3440
+ }
3441
+ /** 1回のポーリングサイクル */
3442
+ async poll() {
3443
+ const queryParams = {
3444
+ limit: 100,
3445
+ ...this.lastEventId ? { afterEventId: this.lastEventId } : {},
3446
+ ...this.parentSessionId ? { parentSessionId: this.parentSessionId, recursive: true } : {}
3447
+ };
3448
+ let events;
3449
+ try {
3450
+ ({ events } = this.eventStore.query(queryParams));
3451
+ } catch {
3452
+ this.lastEventId = null;
3453
+ const retryParams = {
3454
+ limit: 100,
3455
+ ...this.parentSessionId ? { parentSessionId: this.parentSessionId, recursive: true } : {}
3456
+ };
3457
+ ({ events } = this.eventStore.query(retryParams));
3458
+ }
3459
+ if (events.length === 0) return;
3460
+ this.lastEventId = events[events.length - 1].eventId;
3461
+ let updated = false;
3462
+ for (const event of events) {
3463
+ updated = this.processEvent(event) || updated;
3464
+ }
3465
+ if (updated && this.onUpdate) {
3466
+ this.onUpdate(this.getTasks());
3467
+ }
3468
+ }
3469
+ /** AgentEvent → TUITask への変換・更新 */
3470
+ processEvent(event) {
3471
+ const taskId = event.data.taskId ?? event.sessionId;
3472
+ const existing = this.tasks.get(taskId);
3473
+ switch (event.type) {
3474
+ case "session-start": {
3475
+ if (!existing) {
3476
+ const description = this.extractDescription(event);
3477
+ this.tasks.set(taskId, {
3478
+ taskId,
3479
+ sessionId: event.sessionId,
3480
+ backendId: event.backendId,
3481
+ description,
3482
+ status: "running",
3483
+ createdAt: new Date(event.timestamp)
3484
+ });
3485
+ return true;
3486
+ }
3487
+ return false;
3488
+ }
3489
+ case "session-complete": {
3490
+ if (existing) {
3491
+ existing.status = "completed";
3492
+ existing.completedAt = new Date(event.timestamp);
3493
+ return true;
3494
+ }
3495
+ this.tasks.set(taskId, {
3496
+ taskId,
3497
+ sessionId: event.sessionId,
3498
+ backendId: event.backendId,
3499
+ description: this.extractDescription(event),
3500
+ status: "completed",
3501
+ createdAt: new Date(event.timestamp),
3502
+ completedAt: new Date(event.timestamp)
3503
+ });
3504
+ return true;
3505
+ }
3506
+ case "session-error": {
3507
+ if (existing) {
3508
+ existing.status = "failed";
3509
+ existing.completedAt = new Date(event.timestamp);
3510
+ existing.error = event.data.error ?? "Unknown error";
3511
+ return true;
3512
+ }
3513
+ this.tasks.set(taskId, {
3514
+ taskId,
3515
+ sessionId: event.sessionId,
3516
+ backendId: event.backendId,
3517
+ description: this.extractDescription(event),
3518
+ status: "failed",
3519
+ createdAt: new Date(event.timestamp),
3520
+ completedAt: new Date(event.timestamp),
3521
+ error: event.data.error ?? "Unknown error"
3522
+ });
3523
+ return true;
3524
+ }
3525
+ case "session-stale": {
3526
+ if (existing && existing.status === "running") {
3527
+ existing.status = "stale";
3528
+ return true;
3529
+ }
3530
+ return false;
3531
+ }
3532
+ case "context-threshold": {
3533
+ return false;
3534
+ }
3535
+ default:
3536
+ return false;
3537
+ }
3538
+ }
3539
+ /** イベントメタデータから説明を抽出 */
3540
+ extractDescription(event) {
3541
+ const label = event.metadata?.label;
3542
+ if (label) return label;
3543
+ const agentType = event.metadata?.agentType;
3544
+ if (agentType) return agentType;
3545
+ return event.sessionId.slice(0, 12) + "...";
3546
+ }
3547
+ /** 全タスクを取得(新しい順) */
3548
+ getTasks() {
3549
+ return [...this.tasks.values()].sort(
3550
+ (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
3551
+ );
3552
+ }
3553
+ /** アクティブなタスク数を取得 */
3554
+ getActiveCount() {
3555
+ return [...this.tasks.values()].filter(
3556
+ (t) => t.status === "running" || t.status === "pending"
3557
+ ).length;
3558
+ }
3559
+ /** 手動でタスクを追加(会話内のツール実行など) */
3560
+ track(taskId, sessionId, backendId, description) {
3561
+ this.tasks.set(taskId, {
3562
+ taskId,
3563
+ sessionId,
3564
+ backendId,
3565
+ description,
3566
+ status: "running",
3567
+ createdAt: /* @__PURE__ */ new Date()
3568
+ });
3569
+ if (this.onUpdate) {
3570
+ this.onUpdate(this.getTasks());
3571
+ }
3572
+ }
3573
+ /** タスクを手動更新 */
3574
+ updateTask(taskId, status, error) {
3575
+ const task = this.tasks.get(taskId);
3576
+ if (task) {
3577
+ task.status = status;
3578
+ if (status === "completed" || status === "failed") {
3579
+ task.completedAt = /* @__PURE__ */ new Date();
3580
+ }
3581
+ if (error) task.error = error;
3582
+ if (this.onUpdate) {
3583
+ this.onUpdate(this.getTasks());
3584
+ }
3585
+ }
3586
+ }
3587
+ /** クリーンアップ */
3588
+ cleanup() {
3589
+ this.stopPolling();
3590
+ this.tasks.clear();
3591
+ this.lastEventId = null;
3592
+ }
3593
+ };
3594
+ }
3595
+ });
3596
+
3597
+ // src/tui/types.ts
3598
+ var types_exports = {};
3599
+ __export(types_exports, {
3600
+ DEFAULT_TUI_CONFIG: () => DEFAULT_TUI_CONFIG
3601
+ });
3602
+ var DEFAULT_TUI_CONFIG;
3603
+ var init_types2 = __esm({
3604
+ "src/tui/types.ts"() {
3605
+ "use strict";
3606
+ DEFAULT_TUI_CONFIG = {
3607
+ backendSwitch: {
3608
+ strategy: "shared-history",
3609
+ contextSummaryMaxTokens: 2e3
3610
+ },
3611
+ display: {
3612
+ showTokenCount: true,
3613
+ showTimestamp: false
3614
+ },
3615
+ input: {
3616
+ historySize: 100
3617
+ }
3618
+ };
3619
+ }
3620
+ });
3621
+
3622
+ // src/tui/services/tui-config-manager.ts
3623
+ var TUIConfigManager;
3624
+ var init_tui_config_manager = __esm({
3625
+ "src/tui/services/tui-config-manager.ts"() {
3626
+ "use strict";
3627
+ init_types2();
3628
+ TUIConfigManager = class {
3629
+ constructor(configManager2) {
3630
+ this.configManager = configManager2;
3631
+ }
3632
+ /**
3633
+ * Load TUI configuration, deep-merging persisted values over defaults.
3634
+ */
3635
+ async load() {
3636
+ const persisted = await this.configManager.get("tui");
3637
+ if (!persisted) {
3638
+ return { ...DEFAULT_TUI_CONFIG };
3639
+ }
3640
+ return {
3641
+ backendSwitch: {
3642
+ ...DEFAULT_TUI_CONFIG.backendSwitch,
3643
+ ...persisted.backendSwitch ?? {}
3644
+ },
3645
+ display: {
3646
+ ...DEFAULT_TUI_CONFIG.display,
3647
+ ...persisted.display ?? {}
3648
+ },
3649
+ input: {
3650
+ ...DEFAULT_TUI_CONFIG.input,
3651
+ ...persisted.input ?? {}
3652
+ }
3653
+ };
3654
+ }
3655
+ /**
3656
+ * Persist a TUI config value at the project scope.
3657
+ *
3658
+ * @param key Dot-path relative to the `tui` section (e.g. "display.showTokenCount")
3659
+ * @param value The value to store
3660
+ */
3661
+ async set(key, value) {
3662
+ await this.configManager.set("project", `tui.${key}`, value);
3663
+ }
3664
+ };
3665
+ }
3666
+ });
3667
+
3668
+ // src/tui/components/TaskPanel.tsx
3669
+ import { Box as Box6, Text as Text7 } from "ink";
3670
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3671
+ function formatDuration(start, end) {
3672
+ const ms = (end ?? /* @__PURE__ */ new Date()).getTime() - start.getTime();
3673
+ const seconds = Math.floor(ms / 1e3);
3674
+ if (seconds < 60) return `${seconds}s`;
3675
+ const minutes = Math.floor(seconds / 60);
3676
+ if (minutes < 60) return `${minutes}m${seconds % 60}s`;
3677
+ return `${Math.floor(minutes / 60)}h${minutes % 60}m`;
3678
+ }
3679
+ function TaskPanel({ tasks, maxVisible = 10, selectedIndex }) {
3680
+ const visibleTasks = tasks.slice(0, maxVisible);
3681
+ return /* @__PURE__ */ jsxs6(
3682
+ Box6,
3683
+ {
3684
+ flexDirection: "column",
3685
+ width: 25,
3686
+ borderStyle: "single",
3687
+ borderLeft: true,
3688
+ paddingX: 1,
3689
+ children: [
3690
+ /* @__PURE__ */ jsxs6(Box6, { marginBottom: 1, children: [
3691
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Tasks" }),
3692
+ /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
3693
+ " (",
3694
+ tasks.length,
3695
+ ")"
3696
+ ] })
3697
+ ] }),
3698
+ visibleTasks.length === 0 ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "No tasks" }) : visibleTasks.map((task, index) => {
3699
+ const indicator = STATUS_INDICATORS[task.status];
3700
+ const duration = formatDuration(task.createdAt, task.completedAt);
3701
+ const desc = task.description.length > 15 ? task.description.slice(0, 15) + "\u2026" : task.description;
3702
+ const isSelected = index === selectedIndex;
3703
+ const cursor = isSelected ? ">" : " ";
3704
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginBottom: 0, children: [
3705
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3706
+ /* @__PURE__ */ jsx7(Text7, { inverse: isSelected, children: cursor }),
3707
+ /* @__PURE__ */ jsxs6(Text7, { color: indicator.color, inverse: isSelected, children: [
3708
+ indicator.symbol,
3709
+ " "
3710
+ ] }),
3711
+ /* @__PURE__ */ jsx7(Text7, { inverse: isSelected, children: desc })
3712
+ ] }),
3713
+ /* @__PURE__ */ jsx7(Box6, { marginLeft: 2, children: /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
3714
+ BACKEND_SHORT[task.backendId],
3715
+ " ",
3716
+ duration
3717
+ ] }) })
3718
+ ] }, task.taskId);
3719
+ }),
3720
+ tasks.length > maxVisible && /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
3721
+ "+",
3722
+ tasks.length - maxVisible,
3723
+ " more"
3724
+ ] }),
3725
+ /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2191\u2193:select r:retry x:cancel" }) })
3726
+ ]
3727
+ }
3728
+ );
3729
+ }
3730
+ var STATUS_INDICATORS, BACKEND_SHORT;
3731
+ var init_TaskPanel = __esm({
3732
+ "src/tui/components/TaskPanel.tsx"() {
3733
+ "use strict";
3734
+ STATUS_INDICATORS = {
3735
+ pending: { symbol: "\u25CB", color: "gray" },
3736
+ running: { symbol: "\u25CF", color: "yellow" },
3737
+ completed: { symbol: "\u2713", color: "green" },
3738
+ failed: { symbol: "\u2717", color: "red" },
3739
+ stale: { symbol: "\u25CC", color: "gray" }
3740
+ };
3741
+ BACKEND_SHORT = {
3742
+ claude: "CL",
3743
+ codex: "CX",
3744
+ gemini: "GM"
3745
+ };
3746
+ }
3747
+ });
3748
+
3749
+ // src/tui/app.tsx
3750
+ var app_exports = {};
3751
+ __export(app_exports, {
3752
+ App: () => App
3753
+ });
3754
+ import { useState, useEffect, useCallback } from "react";
3755
+ import { Box as Box7, Text as Text8, useInput, useApp } from "ink";
3756
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
3757
+ function App({ registry: registry2, sessionManager: sessionManager2, hooksEngine: hooksEngine2, contextMonitor: contextMonitor2, configManager: configManager2, initialBackend, model }) {
3758
+ const { exit } = useApp();
3759
+ const [messages, setMessages] = useState([]);
3760
+ const [inputValue, setInputValue] = useState("");
3761
+ const [isStreaming, setIsStreaming] = useState(false);
3762
+ const [streamingText, setStreamingText] = useState("");
3763
+ const [activeBackend, setActiveBackend] = useState("claude");
3764
+ const [sessionId, setSessionId] = useState();
3765
+ const [tokenCount, setTokenCount] = useState({ input: 0, output: 0 });
3766
+ const [tools, setTools] = useState([]);
3767
+ const [statusMessage, setStatusMessage] = useState("");
3768
+ const [error, setError] = useState(null);
3769
+ const [isInitialized, setIsInitialized] = useState(false);
3770
+ const [inputHistory, setInputHistory] = useState([]);
3771
+ const [historyIndex, setHistoryIndex] = useState(-1);
3772
+ const [backendStatuses, setBackendStatuses] = useState([]);
3773
+ const [showTasks, setShowTasks] = useState(false);
3774
+ const [tuiTasks, setTuiTasks] = useState([]);
3775
+ const [taskSelectedIndex, setTaskSelectedIndex] = useState(0);
3776
+ const [tuiConfigManager] = useState(() => new TUIConfigManager(configManager2));
3777
+ const [tuiConfig, setTuiConfig] = useState(null);
3778
+ const [engine] = useState(() => new ConversationEngine(registry2, sessionManager2, hooksEngine2, contextMonitor2));
3779
+ const [orchestrator] = useState(() => new BackendOrchestrator(registry2, sessionManager2, hooksEngine2));
3780
+ const [taskPoller] = useState(() => {
3781
+ const eventStore = new AgentEventStore({ backend: "memory" });
3782
+ return new TaskPoller(eventStore);
3783
+ });
3784
+ useEffect(() => {
3785
+ void (async () => {
3786
+ try {
3787
+ const loadedConfig = await tuiConfigManager.load();
3788
+ setTuiConfig(loadedConfig);
3789
+ orchestrator.setStrategy(loadedConfig.backendSwitch.strategy);
3790
+ const backendId = await orchestrator.initialize(initialBackend);
3791
+ setActiveBackend(backendId);
3792
+ const sid = await engine.initSession(backendId);
3793
+ setSessionId(sid);
3794
+ setBackendStatuses(orchestrator.getStatuses());
3795
+ orchestrator.setSessionId(backendId, sid);
3796
+ setIsInitialized(true);
3797
+ } catch (e) {
3798
+ setError(e instanceof Error ? e.message : String(e));
3799
+ }
3800
+ })();
3801
+ }, []);
3802
+ useEffect(() => {
3803
+ if (!isInitialized) return;
3804
+ taskPoller.setOnUpdate((tasks) => {
3805
+ setTuiTasks(tasks);
3806
+ });
3807
+ taskPoller.startPolling(2e3);
3808
+ return () => {
3809
+ taskPoller.cleanup();
3810
+ };
3811
+ }, [isInitialized, taskPoller]);
3812
+ const processInput = useCallback(async (text) => {
3813
+ if (!text.trim() || isStreaming) return;
3814
+ setIsStreaming(true);
3815
+ setStreamingText("");
3816
+ setTools([]);
3817
+ setInputHistory((prev) => [text, ...prev].slice(0, 100));
3818
+ setHistoryIndex(-1);
3819
+ const userMsg = {
3820
+ id: Date.now().toString(),
3821
+ role: "user",
3822
+ content: text,
3823
+ timestamp: /* @__PURE__ */ new Date(),
3824
+ backendId: activeBackend
3825
+ };
3826
+ setMessages((prev) => [...prev, userMsg]);
3827
+ let fullText = "";
3828
+ try {
3829
+ for await (const event of engine.processInput(text, activeBackend)) {
3830
+ switch (event.type) {
3831
+ case "text":
3832
+ fullText += event.text;
3833
+ setStreamingText(fullText);
3834
+ break;
3835
+ case "tool_start":
3836
+ setTools((prev) => [...prev, { id: event.id, tool: event.tool, status: "running" }]);
3837
+ break;
3838
+ case "tool_end":
3839
+ setTools((prev) => prev.map((t) => t.id === event.id ? { ...t, status: event.result ? "completed" : "failed", result: event.result } : t));
3840
+ break;
3841
+ case "usage":
3842
+ setTokenCount((prev) => ({ input: prev.input + event.inputTokens, output: prev.output + event.outputTokens }));
3843
+ break;
3844
+ case "error":
3845
+ setStatusMessage(event.message);
3846
+ break;
3847
+ case "status":
3848
+ setStatusMessage(event.message);
3849
+ break;
3850
+ case "done":
3851
+ break;
3852
+ }
3853
+ }
3854
+ } catch (e) {
3855
+ setStatusMessage(e instanceof Error ? e.message : "An error occurred");
3856
+ }
3857
+ if (fullText) {
3858
+ const assistantMsg = {
3859
+ id: (Date.now() + 1).toString(),
3860
+ role: "assistant",
3861
+ content: fullText,
3862
+ timestamp: /* @__PURE__ */ new Date(),
3863
+ backendId: activeBackend
3864
+ };
3865
+ setMessages((prev) => [...prev, assistantMsg]);
3866
+ }
3867
+ setStreamingText("");
3868
+ setIsStreaming(false);
3869
+ setTokenCount(engine.getTotalTokens());
3870
+ }, [isStreaming, activeBackend, engine]);
3871
+ const switchBackend = useCallback(async (target) => {
3872
+ if (isStreaming) return;
3873
+ if (!orchestrator.isAvailable(target)) {
3874
+ setStatusMessage(`Backend ${target} is not available`);
3875
+ return;
3876
+ }
3877
+ if (orchestrator.getActiveId() === target) return;
3878
+ try {
3879
+ const { contextSummary } = await orchestrator.switchTo(target, engine.getMessages());
3880
+ const sid = await engine.switchBackend(target, contextSummary);
3881
+ setActiveBackend(target);
3882
+ setSessionId(sid);
3883
+ setTokenCount({ input: 0, output: 0 });
3884
+ orchestrator.setSessionId(target, sid);
3885
+ setBackendStatuses(orchestrator.getStatuses());
3886
+ setMessages((prev) => [...prev, {
3887
+ id: Date.now().toString(),
3888
+ role: "system",
3889
+ content: `Switched to ${target}${contextSummary ? " (with context)" : ""}`,
3890
+ timestamp: /* @__PURE__ */ new Date(),
3891
+ backendId: target
3892
+ }]);
3893
+ setStatusMessage(`Switched to ${target}`);
3894
+ } catch (e) {
3895
+ setStatusMessage(e instanceof Error ? e.message : "Switch failed");
3896
+ }
3897
+ }, [isStreaming, engine, orchestrator]);
3898
+ const handleSlashCommand = useCallback(async (text) => {
3899
+ const [cmd, ...argParts] = text.slice(1).split(" ");
3900
+ const args = argParts.join(" ");
3901
+ switch (cmd) {
3902
+ case "help":
3903
+ setMessages((prev) => [...prev, {
3904
+ id: Date.now().toString(),
3905
+ role: "system",
3906
+ content: "Available commands:\n/help - Show this help\n/clear - Clear messages\n/backend <name> - Switch backend\n/model - Show current model\n/config [show|set <key> <value>|reset] - Manage TUI config\n/exit - Exit TUI",
3907
+ timestamp: /* @__PURE__ */ new Date(),
3908
+ backendId: activeBackend
3909
+ }]);
3910
+ break;
3911
+ case "clear":
3912
+ setMessages([]);
3913
+ engine.clearMessages();
3914
+ break;
3915
+ case "backend":
3916
+ if (args && ["claude", "codex", "gemini"].includes(args)) {
3917
+ void switchBackend(args);
3918
+ } else {
3919
+ setStatusMessage("Usage: /backend <claude|codex|gemini>");
3920
+ }
3921
+ break;
3922
+ case "model":
3923
+ setMessages((prev) => [...prev, {
3924
+ id: Date.now().toString(),
3925
+ role: "system",
3926
+ content: `Current backend: ${activeBackend}${model ? `, model: ${model}` : ""}`,
3927
+ timestamp: /* @__PURE__ */ new Date(),
3928
+ backendId: activeBackend
3929
+ }]);
3930
+ break;
3931
+ case "config": {
3932
+ const [subCmd, ...rest] = args.split(" ");
3933
+ if (subCmd === "show" || !subCmd) {
3934
+ const currentConfig = await tuiConfigManager.load();
3935
+ setMessages((prev) => [...prev, {
3936
+ id: Date.now().toString(),
3937
+ role: "system",
3938
+ content: "TUI Config:\n```json\n" + JSON.stringify(currentConfig, null, 2) + "\n```",
3939
+ timestamp: /* @__PURE__ */ new Date(),
3940
+ backendId: activeBackend
3941
+ }]);
3942
+ } else if (subCmd === "set" && rest.length >= 2) {
3943
+ const key = rest[0];
3944
+ const rawValue = rest.slice(1).join(" ");
3945
+ let value = rawValue;
3946
+ if (rawValue === "true") value = true;
3947
+ else if (rawValue === "false") value = false;
3948
+ else if (!isNaN(Number(rawValue))) value = Number(rawValue);
3949
+ await tuiConfigManager.set(key, value);
3950
+ const updated = await tuiConfigManager.load();
3951
+ setTuiConfig(updated);
3952
+ setStatusMessage(`Config updated: ${key} = ${JSON.stringify(value)}`);
3953
+ } else if (subCmd === "reset") {
3954
+ const { DEFAULT_TUI_CONFIG: DEFAULT_TUI_CONFIG2 } = await Promise.resolve().then(() => (init_types2(), types_exports));
3955
+ for (const [section, sectionValue] of Object.entries(DEFAULT_TUI_CONFIG2)) {
3956
+ for (const [k, v] of Object.entries(sectionValue)) {
3957
+ await tuiConfigManager.set(`${section}.${k}`, v);
3958
+ }
3959
+ }
3960
+ const updated = await tuiConfigManager.load();
3961
+ setTuiConfig(updated);
3962
+ setStatusMessage("Config reset to defaults");
3963
+ } else {
3964
+ setStatusMessage("Usage: /config [show|set <key> <value>|reset]");
3965
+ }
3966
+ break;
3967
+ }
3968
+ case "exit":
3969
+ exit();
3970
+ break;
3971
+ default:
3972
+ setStatusMessage(`Unknown command: /${cmd}`);
3973
+ }
3974
+ }, [activeBackend, model, engine, switchBackend, exit, tuiConfigManager]);
3975
+ const handleSubmit = useCallback((text) => {
3976
+ if (!text.trim()) return;
3977
+ setInputValue("");
3978
+ if (text.startsWith("/")) {
3979
+ void handleSlashCommand(text);
3980
+ } else {
3981
+ void processInput(text);
3982
+ }
3983
+ }, [processInput, handleSlashCommand]);
3984
+ useInput((input, key) => {
3985
+ if (key.ctrl && input === "c") {
3986
+ if (isStreaming) {
3987
+ setIsStreaming(false);
3988
+ setStreamingText("");
3989
+ setStatusMessage("Cancelled");
3990
+ } else {
3991
+ exit();
3992
+ }
3993
+ return;
3994
+ }
3995
+ if (key.ctrl && input === "d") {
3996
+ exit();
3997
+ return;
3998
+ }
3999
+ if (key.ctrl && input === "l") {
4000
+ setMessages([]);
4001
+ engine.clearMessages();
4002
+ return;
4003
+ }
4004
+ if (key.ctrl && input === "1") {
4005
+ void switchBackend("claude");
4006
+ return;
4007
+ }
4008
+ if (key.ctrl && input === "2") {
4009
+ void switchBackend("codex");
4010
+ return;
4011
+ }
4012
+ if (key.ctrl && input === "3") {
4013
+ void switchBackend("gemini");
4014
+ return;
4015
+ }
4016
+ if (key.ctrl && input === "t") {
4017
+ setShowTasks((prev) => !prev);
4018
+ return;
4019
+ }
4020
+ if (showTasks) {
4021
+ if (key.upArrow) {
4022
+ setTaskSelectedIndex((prev) => Math.max(0, prev - 1));
4023
+ return;
4024
+ }
4025
+ if (key.downArrow) {
4026
+ setTaskSelectedIndex((prev) => Math.min(tuiTasks.length - 1, prev + 1));
4027
+ return;
4028
+ }
4029
+ if (input === "r") {
4030
+ const selectedTask = tuiTasks[taskSelectedIndex];
4031
+ if (selectedTask && (selectedTask.status === "failed" || selectedTask.status === "stale")) {
4032
+ taskPoller.updateTask(selectedTask.taskId, "pending");
4033
+ }
4034
+ return;
4035
+ }
4036
+ if (input === "x") {
4037
+ const selectedTask = tuiTasks[taskSelectedIndex];
4038
+ if (selectedTask && selectedTask.status === "running") {
4039
+ taskPoller.updateTask(selectedTask.taskId, "failed", "Cancelled by user");
4040
+ }
4041
+ return;
4042
+ }
4043
+ } else {
4044
+ if (key.upArrow && !isStreaming && inputHistory.length > 0) {
4045
+ const newIndex = Math.min(historyIndex + 1, inputHistory.length - 1);
4046
+ setHistoryIndex(newIndex);
4047
+ setInputValue(inputHistory[newIndex]);
4048
+ return;
4049
+ }
4050
+ if (key.downArrow && !isStreaming) {
4051
+ const newIndex = historyIndex - 1;
4052
+ if (newIndex < 0) {
4053
+ setHistoryIndex(-1);
4054
+ setInputValue("");
4055
+ } else {
4056
+ setHistoryIndex(newIndex);
4057
+ setInputValue(inputHistory[newIndex]);
4058
+ }
4059
+ return;
4060
+ }
4061
+ }
4062
+ });
4063
+ if (error) {
4064
+ return /* @__PURE__ */ jsx8(Box7, { children: /* @__PURE__ */ jsxs7(Text8, { color: "red", children: [
4065
+ "Error: ",
4066
+ error
4067
+ ] }) });
4068
+ }
4069
+ if (!isInitialized) {
4070
+ return /* @__PURE__ */ jsx8(Box7, { children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Initializing backends..." }) });
4071
+ }
4072
+ const activeTaskCount = tuiTasks.filter((t) => t.status === "running" || t.status === "pending").length;
4073
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", height: "100%", children: [
4074
+ /* @__PURE__ */ jsx8(
4075
+ StatusBar,
4076
+ {
4077
+ backendId: activeBackend,
4078
+ model,
4079
+ sessionId,
4080
+ tokenCount,
4081
+ isStreaming,
4082
+ showTokenCount: tuiConfig?.display.showTokenCount
4083
+ }
4084
+ ),
4085
+ /* @__PURE__ */ jsxs7(Box7, { flexGrow: 1, flexDirection: "row", children: [
4086
+ /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", flexGrow: 1, children: [
4087
+ /* @__PURE__ */ jsx8(
4088
+ ChatView,
4089
+ {
4090
+ messages,
4091
+ streamingText,
4092
+ isStreaming
4093
+ }
4094
+ ),
4095
+ tools.length > 0 && /* @__PURE__ */ jsx8(ToolProgress, { tools })
4096
+ ] }),
4097
+ showTasks && /* @__PURE__ */ jsx8(
4098
+ TaskPanel,
4099
+ {
4100
+ tasks: tuiTasks,
4101
+ selectedIndex: taskSelectedIndex,
4102
+ onRetry: (task) => {
4103
+ taskPoller.updateTask(task.taskId, "pending");
4104
+ },
4105
+ onCancel: (task) => {
4106
+ taskPoller.updateTask(task.taskId, "failed", "Cancelled by user");
4107
+ }
4108
+ }
4109
+ )
4110
+ ] }),
4111
+ statusMessage && /* @__PURE__ */ jsx8(Box7, { paddingX: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: statusMessage }) }),
4112
+ /* @__PURE__ */ jsx8(
4113
+ InputArea,
4114
+ {
4115
+ value: inputValue,
4116
+ onChange: setInputValue,
4117
+ onSubmit: handleSubmit,
4118
+ isDisabled: isStreaming
4119
+ }
4120
+ ),
4121
+ /* @__PURE__ */ jsxs7(Box7, { children: [
4122
+ /* @__PURE__ */ jsx8(
4123
+ BackendIndicator,
4124
+ {
4125
+ statuses: backendStatuses,
4126
+ activeBackendId: activeBackend
4127
+ }
4128
+ ),
4129
+ /* @__PURE__ */ jsxs7(Box7, { marginLeft: 2, children: [
4130
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "^T " }),
4131
+ /* @__PURE__ */ jsx8(Text8, { bold: showTasks, children: "Tasks" }),
4132
+ activeTaskCount > 0 && /* @__PURE__ */ jsxs7(Text8, { color: "yellow", children: [
4133
+ "(",
4134
+ activeTaskCount,
4135
+ ")"
4136
+ ] })
4137
+ ] })
4138
+ ] })
4139
+ ] });
4140
+ }
4141
+ var init_app = __esm({
4142
+ "src/tui/app.tsx"() {
4143
+ "use strict";
4144
+ init_conversation_engine();
4145
+ init_backend_orchestrator();
4146
+ init_StatusBar();
4147
+ init_ChatView();
4148
+ init_InputArea();
4149
+ init_ToolProgress();
4150
+ init_BackendIndicator();
4151
+ init_agent_event_store();
4152
+ init_task_poller();
4153
+ init_tui_config_manager();
4154
+ init_TaskPanel();
4155
+ }
4156
+ });
4157
+
2898
4158
  // src/bin/relay.ts
2899
- import { defineCommand as defineCommand10, runMain } from "citty";
4159
+ import { defineCommand as defineCommand11, runMain } from "citty";
2900
4160
  import { join as join11 } from "path";
2901
4161
  import { homedir as homedir7 } from "os";
2902
4162
 
@@ -4617,7 +5877,20 @@ var relayConfigSchema = z.object({
4617
5877
  telemetry: z.object({
4618
5878
  enabled: z.boolean()
4619
5879
  }).optional(),
4620
- claudePermissionMode: z.enum(["default", "bypassPermissions"]).optional()
5880
+ claudePermissionMode: z.enum(["default", "bypassPermissions"]).optional(),
5881
+ tui: z.object({
5882
+ backendSwitch: z.object({
5883
+ strategy: z.enum(["isolated", "shared-history", "full-transfer"]).optional(),
5884
+ contextSummaryMaxTokens: z.number().positive().optional()
5885
+ }).optional(),
5886
+ display: z.object({
5887
+ showTokenCount: z.boolean().optional(),
5888
+ showTimestamp: z.boolean().optional()
5889
+ }).optional(),
5890
+ input: z.object({
5891
+ historySize: z.number().int().positive().optional()
5892
+ }).optional()
5893
+ }).optional()
4621
5894
  });
4622
5895
 
4623
5896
  // src/core/config-manager.ts
@@ -6212,7 +7485,7 @@ function createVersionCommand(registry2) {
6212
7485
  description: "Show relay and backend versions"
6213
7486
  },
6214
7487
  async run() {
6215
- const relayVersion = "1.3.1";
7488
+ const relayVersion = "1.4.1";
6216
7489
  console.log(`agentic-relay v${relayVersion}`);
6217
7490
  console.log("");
6218
7491
  console.log("Backends:");
@@ -6536,6 +7809,48 @@ function createInitCommand() {
6536
7809
  });
6537
7810
  }
6538
7811
 
7812
+ // src/commands/interactive.ts
7813
+ import { defineCommand as defineCommand10 } from "citty";
7814
+ function createInteractiveCommand(registry2, sessionManager2, hooksEngine2, contextMonitor2, configManager2) {
7815
+ return defineCommand10({
7816
+ meta: {
7817
+ name: "interactive",
7818
+ description: "Start interactive TUI mode"
7819
+ },
7820
+ args: {
7821
+ backend: {
7822
+ type: "string",
7823
+ alias: "b",
7824
+ description: "Initial backend (claude, codex, gemini)"
7825
+ },
7826
+ model: {
7827
+ type: "string",
7828
+ alias: "m",
7829
+ description: "Model to use"
7830
+ }
7831
+ },
7832
+ async run({ args }) {
7833
+ const { render } = await import("ink");
7834
+ const { createElement } = await import("react");
7835
+ const { App: App2 } = await Promise.resolve().then(() => (init_app(), app_exports));
7836
+ const initialBackend = args.backend;
7837
+ const model = args.model;
7838
+ const { waitUntilExit } = render(
7839
+ createElement(App2, {
7840
+ registry: registry2,
7841
+ sessionManager: sessionManager2,
7842
+ hooksEngine: hooksEngine2,
7843
+ contextMonitor: contextMonitor2,
7844
+ configManager: configManager2,
7845
+ initialBackend,
7846
+ model
7847
+ })
7848
+ );
7849
+ await waitUntilExit();
7850
+ }
7851
+ });
7852
+ }
7853
+
6539
7854
  // src/bin/relay.ts
6540
7855
  init_logger();
6541
7856
  var processManager = new ProcessManager();
@@ -6562,12 +7877,40 @@ void configManager.getConfig().then((config) => {
6562
7877
  });
6563
7878
  }
6564
7879
  }).catch((e) => logger.debug("Config load failed:", e));
6565
- var main = defineCommand10({
7880
+ var interactiveCmd = createInteractiveCommand(registry, sessionManager, hooksEngine, contextMonitor, configManager);
7881
+ var subCommandNames = /* @__PURE__ */ new Set(["claude", "codex", "gemini", "update", "config", "mcp", "auth", "sessions", "version", "doctor", "init"]);
7882
+ var main = defineCommand11({
6566
7883
  meta: {
6567
7884
  name: "relay",
6568
- version: "1.3.1",
7885
+ version: "1.4.1",
6569
7886
  description: "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI"
6570
7887
  },
7888
+ args: {
7889
+ backend: {
7890
+ type: "string",
7891
+ alias: "b",
7892
+ description: "Initial backend for interactive mode (claude, codex, gemini)"
7893
+ },
7894
+ model: {
7895
+ type: "string",
7896
+ alias: "m",
7897
+ description: "Model to use in interactive mode"
7898
+ }
7899
+ },
7900
+ async run({ args }) {
7901
+ const hasSubCommand = process.argv.slice(2).some((arg) => subCommandNames.has(arg));
7902
+ if (hasSubCommand) return;
7903
+ const interactiveArgs = {
7904
+ backend: args.backend ?? "",
7905
+ model: args.model ?? ""
7906
+ };
7907
+ await interactiveCmd.run({
7908
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7909
+ args: interactiveArgs,
7910
+ rawArgs: [],
7911
+ cmd: interactiveCmd
7912
+ });
7913
+ },
6571
7914
  subCommands: {
6572
7915
  claude: createBackendCommand("claude", registry, sessionManager, hooksEngine, contextMonitor),
6573
7916
  codex: createBackendCommand("codex", registry, sessionManager, hooksEngine, contextMonitor),