@rk0429/agentic-relay 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/relay.mjs +1357 -18
- 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.
|
|
2381
|
+
{ name: "agentic-relay", version: "1.4.0" },
|
|
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,
|
|
@@ -2647,7 +2666,7 @@ var init_server = __esm({
|
|
|
2647
2666
|
);
|
|
2648
2667
|
server.tool(
|
|
2649
2668
|
"check_session_health",
|
|
2650
|
-
"Check relay session
|
|
2669
|
+
"Check whether a relay session is alive, stale, or completed. Use to verify sub-agent progress.",
|
|
2651
2670
|
checkSessionHealthInputSchema.shape,
|
|
2652
2671
|
async (params) => {
|
|
2653
2672
|
try {
|
|
@@ -2677,7 +2696,7 @@ var init_server = __esm({
|
|
|
2677
2696
|
);
|
|
2678
2697
|
server.tool(
|
|
2679
2698
|
"poll_agent_events",
|
|
2680
|
-
"
|
|
2699
|
+
"Get agent lifecycle events (spawned, completed, failed) since a cursor. Use for monitoring sub-agent progress.",
|
|
2681
2700
|
pollAgentEventsInputSchema.shape,
|
|
2682
2701
|
async (params) => {
|
|
2683
2702
|
try {
|
|
@@ -2710,7 +2729,7 @@ var init_server = __esm({
|
|
|
2710
2729
|
);
|
|
2711
2730
|
server.tool(
|
|
2712
2731
|
"get_context_status",
|
|
2713
|
-
"Get
|
|
2732
|
+
"Get context window usage of a relay session. Use to check if a sub-agent is approaching context limits.",
|
|
2714
2733
|
{
|
|
2715
2734
|
sessionId: z7.string().describe("Relay session ID to query context usage for.")
|
|
2716
2735
|
},
|
|
@@ -2740,7 +2759,7 @@ var init_server = __esm({
|
|
|
2740
2759
|
);
|
|
2741
2760
|
server.tool(
|
|
2742
2761
|
"list_available_backends",
|
|
2743
|
-
"List
|
|
2762
|
+
"List registered backends and their health. spawn_agent auto-selects backends, so this is mainly for diagnostics.",
|
|
2744
2763
|
{},
|
|
2745
2764
|
async () => {
|
|
2746
2765
|
try {
|
|
@@ -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.
|
|
2852
|
+
{ name: "agentic-relay", version: "1.4.0" },
|
|
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
|
|
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.
|
|
7488
|
+
const relayVersion = "1.4.0";
|
|
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,36 @@ void configManager.getConfig().then((config) => {
|
|
|
6562
7877
|
});
|
|
6563
7878
|
}
|
|
6564
7879
|
}).catch((e) => logger.debug("Config load failed:", e));
|
|
6565
|
-
var
|
|
7880
|
+
var interactiveCmd = createInteractiveCommand(registry, sessionManager, hooksEngine, contextMonitor, configManager);
|
|
7881
|
+
var main = defineCommand11({
|
|
6566
7882
|
meta: {
|
|
6567
7883
|
name: "relay",
|
|
6568
|
-
version: "1.
|
|
7884
|
+
version: "1.4.0",
|
|
6569
7885
|
description: "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI"
|
|
6570
7886
|
},
|
|
7887
|
+
args: {
|
|
7888
|
+
backend: {
|
|
7889
|
+
type: "string",
|
|
7890
|
+
alias: "b",
|
|
7891
|
+
description: "Initial backend for interactive mode (claude, codex, gemini)"
|
|
7892
|
+
},
|
|
7893
|
+
model: {
|
|
7894
|
+
type: "string",
|
|
7895
|
+
alias: "m",
|
|
7896
|
+
description: "Model to use in interactive mode"
|
|
7897
|
+
}
|
|
7898
|
+
},
|
|
7899
|
+
async run({ args }) {
|
|
7900
|
+
const interactiveArgs = {
|
|
7901
|
+
backend: args.backend ?? "",
|
|
7902
|
+
model: args.model ?? ""
|
|
7903
|
+
};
|
|
7904
|
+
await interactiveCmd.run({
|
|
7905
|
+
args: interactiveArgs,
|
|
7906
|
+
rawArgs: [],
|
|
7907
|
+
cmd: interactiveCmd
|
|
7908
|
+
});
|
|
7909
|
+
},
|
|
6571
7910
|
subCommands: {
|
|
6572
7911
|
claude: createBackendCommand("claude", registry, sessionManager, hooksEngine, contextMonitor),
|
|
6573
7912
|
codex: createBackendCommand("codex", registry, sessionManager, hooksEngine, contextMonitor),
|