govon 1.2.2 → 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.
Files changed (80) hide show
  1. package/bin/govon.js +2 -41
  2. package/dist/App.d.ts +22 -0
  3. package/dist/App.d.ts.map +1 -0
  4. package/dist/App.js +307 -0
  5. package/dist/App.js.map +1 -0
  6. package/dist/client.d.ts +102 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +332 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/components/ApprovalPrompt.d.ts +15 -0
  11. package/dist/components/ApprovalPrompt.d.ts.map +1 -0
  12. package/dist/components/ApprovalPrompt.js +35 -0
  13. package/dist/components/ApprovalPrompt.js.map +1 -0
  14. package/dist/components/Banner.d.ts +6 -0
  15. package/dist/components/Banner.d.ts.map +1 -0
  16. package/dist/components/Banner.js +27 -0
  17. package/dist/components/Banner.js.map +1 -0
  18. package/dist/components/InputBar.d.ts +8 -0
  19. package/dist/components/InputBar.d.ts.map +1 -0
  20. package/dist/components/InputBar.js +26 -0
  21. package/dist/components/InputBar.js.map +1 -0
  22. package/dist/components/MarkdownView.d.ts +9 -0
  23. package/dist/components/MarkdownView.d.ts.map +1 -0
  24. package/dist/components/MarkdownView.js +38 -0
  25. package/dist/components/MarkdownView.js.map +1 -0
  26. package/dist/components/MessageBubble.d.ts +14 -0
  27. package/dist/components/MessageBubble.d.ts.map +1 -0
  28. package/dist/components/MessageBubble.js +15 -0
  29. package/dist/components/MessageBubble.js.map +1 -0
  30. package/dist/components/MetadataBar.d.ts +7 -0
  31. package/dist/components/MetadataBar.d.ts.map +1 -0
  32. package/dist/components/MetadataBar.js +19 -0
  33. package/dist/components/MetadataBar.js.map +1 -0
  34. package/dist/components/Spinner.d.ts +7 -0
  35. package/dist/components/Spinner.d.ts.map +1 -0
  36. package/dist/components/Spinner.js +51 -0
  37. package/dist/components/Spinner.js.map +1 -0
  38. package/dist/components/ThinkingBlock.d.ts +9 -0
  39. package/dist/components/ThinkingBlock.d.ts.map +1 -0
  40. package/dist/components/ThinkingBlock.js +9 -0
  41. package/dist/components/ThinkingBlock.js.map +1 -0
  42. package/dist/components/ToolPanel.d.ts +7 -0
  43. package/dist/components/ToolPanel.d.ts.map +1 -0
  44. package/dist/components/ToolPanel.js +19 -0
  45. package/dist/components/ToolPanel.js.map +1 -0
  46. package/dist/config.d.ts +32 -0
  47. package/dist/config.d.ts.map +1 -0
  48. package/dist/config.js +64 -0
  49. package/dist/config.js.map +1 -0
  50. package/dist/data/spinnerVerbs.d.ts +7 -0
  51. package/dist/data/spinnerVerbs.d.ts.map +1 -0
  52. package/dist/data/spinnerVerbs.js +58 -0
  53. package/dist/data/spinnerVerbs.js.map +1 -0
  54. package/dist/hooks/useDaemon.d.ts +16 -0
  55. package/dist/hooks/useDaemon.d.ts.map +1 -0
  56. package/dist/hooks/useDaemon.js +55 -0
  57. package/dist/hooks/useDaemon.js.map +1 -0
  58. package/dist/hooks/useHistory.d.ts +13 -0
  59. package/dist/hooks/useHistory.d.ts.map +1 -0
  60. package/dist/hooks/useHistory.js +170 -0
  61. package/dist/hooks/useHistory.js.map +1 -0
  62. package/dist/hooks/useSSE.d.ts +29 -0
  63. package/dist/hooks/useSSE.d.ts.map +1 -0
  64. package/dist/hooks/useSSE.js +341 -0
  65. package/dist/hooks/useSSE.js.map +1 -0
  66. package/dist/hooks/useTheme.d.ts +39 -0
  67. package/dist/hooks/useTheme.d.ts.map +1 -0
  68. package/dist/hooks/useTheme.js +36 -0
  69. package/dist/hooks/useTheme.js.map +1 -0
  70. package/dist/index.d.ts +7 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +1496 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/types.d.ts +356 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +67 -0
  77. package/dist/types.js.map +1 -0
  78. package/package.json +45 -19
  79. package/README.md +0 -45
  80. package/lib/python-check.js +0 -223
package/dist/index.js ADDED
@@ -0,0 +1,1496 @@
1
+ // src/index.tsx
2
+ import { render } from "ink";
3
+ import yargs from "yargs";
4
+ import { hideBin } from "yargs/helpers";
5
+ import { createRequire } from "node:module";
6
+
7
+ // src/App.tsx
8
+ import React5, { useReducer as useReducer2, useCallback as useCallback5, useMemo as useMemo2 } from "react";
9
+ import { Box as Box9, Text as Text10, Static, useApp, useInput as useInput2, useStdout } from "ink";
10
+
11
+ // src/client.ts
12
+ import { EventSourceParserStream } from "eventsource-parser/stream";
13
+
14
+ // src/config.ts
15
+ function parsePosInt(envVal, defaultVal, maxVal = 86400) {
16
+ const parsed = parseInt(envVal ?? String(defaultVal), 10);
17
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > maxVal) {
18
+ return defaultVal;
19
+ }
20
+ return parsed;
21
+ }
22
+ function getBaseUrl() {
23
+ const raw = (process.env.GOVON_RUNTIME_URL ?? "http://127.0.0.1:8000").replace(/\/+$/, "");
24
+ try {
25
+ const parsed = new URL(raw);
26
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
27
+ throw new Error(`GOVON_RUNTIME_URL must use http or https scheme, got: ${parsed.protocol}`);
28
+ }
29
+ if (parsed.hostname !== "127.0.0.1" && parsed.hostname !== "localhost" && parsed.protocol === "http:") {
30
+ process.stderr.write(
31
+ "[warn] GOVON_RUNTIME_URL uses http:// for a non-localhost host. Use https:// to protect query content in transit.\n"
32
+ );
33
+ }
34
+ return raw;
35
+ } catch (err) {
36
+ if (err instanceof Error && err.message.startsWith("GOVON_RUNTIME_URL")) throw err;
37
+ throw new Error(`GOVON_RUNTIME_URL is not a valid URL: ${raw}`);
38
+ }
39
+ }
40
+ var TIMEOUTS = {
41
+ connect: 1e4,
42
+ read: 3e5,
43
+ /** Blocking /v2/agent/run and /v3/agent/run request timeout. */
44
+ run: 12e4,
45
+ /** Default timeout for simple requests (health, approve, cancel). */
46
+ default: 3e4,
47
+ coldStart: parsePosInt(process.env.GOVON_COLD_START_TIMEOUT, 600) * 1e3,
48
+ coldStartInterval: parsePosInt(process.env.GOVON_COLD_START_INTERVAL, 5) * 1e3
49
+ };
50
+ var THEME_COLORS = {
51
+ primary: "#2D5A3D",
52
+ accent: "#F5E6C8",
53
+ muted: "#7A8B7E",
54
+ error: "#D32F2F",
55
+ warning: "#FFA000",
56
+ success: "#388E3C",
57
+ info: "#1976D2",
58
+ dimmed: "#666666"
59
+ };
60
+ var SPINNER = {
61
+ fps: 30,
62
+ verbChangeInterval: 90,
63
+ // frames (~3 seconds at 30fps)
64
+ chars: ["\xB7", "\u273B", "\u273D", "\u2736", "\u2733", "\u2722"]
65
+ };
66
+
67
+ // src/client.ts
68
+ function makeSignal(ms) {
69
+ return AbortSignal.timeout(ms);
70
+ }
71
+ var GovOnClient = class _GovOnClient {
72
+ _baseUrl;
73
+ /** Cold-start polling timeout in ms (from config). */
74
+ static _COLD_START_TIMEOUT_MS = TIMEOUTS.coldStart;
75
+ /** Cold-start polling interval in ms (from config). */
76
+ static _COLD_START_INTERVAL_MS = TIMEOUTS.coldStartInterval;
77
+ /**
78
+ * @param baseUrl - Daemon base URL (e.g. "http://127.0.0.1:8000").
79
+ * Trailing slash is stripped automatically.
80
+ */
81
+ constructor(baseUrl = getBaseUrl()) {
82
+ this._baseUrl = baseUrl.replace(/\/+$/, "");
83
+ }
84
+ // -------------------------------------------------------------------------
85
+ // Public API
86
+ // -------------------------------------------------------------------------
87
+ /**
88
+ * GET /health — check daemon status.
89
+ *
90
+ * @returns Health response from the server.
91
+ * @throws Error when daemon is unreachable.
92
+ */
93
+ async health() {
94
+ return this._get("/health", TIMEOUTS.default);
95
+ }
96
+ /**
97
+ * Poll GET /health until the server responds with HTTP 200.
98
+ *
99
+ * Handles cold-start / sleeping remote servers (e.g. HF Space) by
100
+ * printing Korean status messages to stderr and waiting patiently.
101
+ *
102
+ * @returns true when server is ready, false on timeout.
103
+ */
104
+ async waitForReady() {
105
+ const url = `${this._baseUrl}/health`;
106
+ const deadline = Date.now() + _GovOnClient._COLD_START_TIMEOUT_MS;
107
+ const intervalMs = _GovOnClient._COLD_START_INTERVAL_MS;
108
+ let lastStatus = "";
109
+ let attempt = 0;
110
+ while (Date.now() < deadline) {
111
+ let newStatus;
112
+ try {
113
+ const signal = makeSignal(1e4);
114
+ const resp = await fetch(url, { signal });
115
+ if (resp.status === 200) {
116
+ if (attempt > 0) {
117
+ process.stderr.write("\r\u2726 \uC11C\uBC84 \uC900\uBE44 \uC644\uB8CC. \n");
118
+ }
119
+ return true;
120
+ }
121
+ if (resp.status === 503) {
122
+ newStatus = "\u229B \uC11C\uBC84 \uC2DC\uC791 \uC911\u2026 (503 \uC751\uB2F5 \uB300\uAE30)";
123
+ } else {
124
+ newStatus = `\u229B \uC11C\uBC84 \uC751\uB2F5 \uB300\uAE30 \uC911\u2026 (HTTP ${resp.status})`;
125
+ }
126
+ } catch (err) {
127
+ const msg = err instanceof Error ? err.message : String(err);
128
+ if (msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND") || msg.includes("fetch failed") || msg.includes("connect")) {
129
+ newStatus = "\u229B \uC11C\uBC84\uC5D0 \uC5F0\uACB0 \uC911\u2026 (sleeping \uC0C1\uD0DC\uC5D0\uC11C \uAE68\uC5B4\uB098\uB294 \uC911)";
130
+ } else if (msg.includes("TimeoutError") || msg.includes("timed out") || msg.includes("abort")) {
131
+ newStatus = "\u229B \uC11C\uBC84 \uC751\uB2F5 \uB300\uAE30 \uC911\u2026 (\uBE4C\uB4DC \uB610\uB294 \uBAA8\uB378 \uB85C\uB529 \uC911)";
132
+ } else {
133
+ newStatus = "\u229B \uC11C\uBC84\uC5D0 \uC5F0\uACB0 \uC911\u2026 (sleeping \uC0C1\uD0DC\uC5D0\uC11C \uAE68\uC5B4\uB098\uB294 \uC911)";
134
+ }
135
+ }
136
+ const elapsed = Math.round((Date.now() - (deadline - _GovOnClient._COLD_START_TIMEOUT_MS)) / 1e3);
137
+ if (newStatus !== lastStatus || attempt % 6 === 0) {
138
+ process.stderr.write(`\r\u23F3 ${newStatus} (${elapsed}s)`);
139
+ lastStatus = newStatus;
140
+ }
141
+ attempt += 1;
142
+ await sleep(intervalMs);
143
+ }
144
+ process.stderr.write("\r\u2718 \uC11C\uBC84 \uC5F0\uACB0 \uC2DC\uAC04 \uCD08\uACFC. \n");
145
+ return false;
146
+ }
147
+ /**
148
+ * POST /v3/agent/stream — v3 ReAct fine-grained SSE streaming.
149
+ *
150
+ * @param query - User input query.
151
+ * @param sessionId - Session ID to resume an existing session.
152
+ * @param maxIterations - Maximum ReAct loop iterations.
153
+ * @param signal - Optional external AbortSignal; composed with the internal timeout.
154
+ * @yields Parsed SSE event objects. Use the `type` key to distinguish them.
155
+ */
156
+ async *streamV3(query2, sessionId, maxIterations, signal) {
157
+ const body = { query: query2 };
158
+ if (sessionId !== void 0) body["session_id"] = sessionId;
159
+ if (maxIterations !== void 0) body["max_iterations"] = maxIterations;
160
+ const url = `${this._baseUrl}/v3/agent/stream`;
161
+ yield* this._sseStream(url, body, "stream_v3", signal);
162
+ }
163
+ /**
164
+ * POST /v2/agent/stream — per-node SSE streaming.
165
+ *
166
+ * @param query - User input query.
167
+ * @param sessionId - Session ID to resume an existing session.
168
+ * @param signal - Optional external AbortSignal; composed with the internal timeout.
169
+ * @yields Parsed SSE event objects. Contains at least `node` and `status` keys.
170
+ */
171
+ async *stream(query2, sessionId, signal) {
172
+ const body = { query: query2 };
173
+ if (sessionId !== void 0) body["session_id"] = sessionId;
174
+ const url = `${this._baseUrl}/v2/agent/stream`;
175
+ yield* this._sseStream(url, body, "stream", signal);
176
+ }
177
+ /**
178
+ * POST /v2/agent/run — blocking agent execution.
179
+ *
180
+ * @param query - User input query.
181
+ * @param sessionId - Session ID to resume an existing session.
182
+ * @param signal - Optional external AbortSignal; composed with the internal timeout.
183
+ * @returns Server response including thread_id, status, etc.
184
+ */
185
+ async run(query2, sessionId, signal) {
186
+ const body = { query: query2 };
187
+ if (sessionId !== void 0) body["session_id"] = sessionId;
188
+ return this._post("/v2/agent/run", body, TIMEOUTS.run, signal);
189
+ }
190
+ /**
191
+ * POST /v3/agent/run — v3 ReAct blocking execution.
192
+ *
193
+ * @param query - User input query.
194
+ * @param sessionId - Session ID to resume an existing session.
195
+ * @param maxIterations - Maximum ReAct loop iterations.
196
+ * @param signal - Optional external AbortSignal; composed with the internal timeout.
197
+ * @returns Server response including metadata.
198
+ */
199
+ async runV3(query2, sessionId, maxIterations, signal) {
200
+ const body = { query: query2 };
201
+ if (sessionId !== void 0) body["session_id"] = sessionId;
202
+ if (maxIterations !== void 0) body["max_iterations"] = maxIterations;
203
+ return this._post("/v3/agent/run", body, TIMEOUTS.run, signal);
204
+ }
205
+ /**
206
+ * POST /v2/agent/approve — approve or reject a pending tool call.
207
+ *
208
+ * Note: uses QUERY PARAMETERS (thread_id, approved), not request body.
209
+ *
210
+ * @param threadId - Graph thread ID to approve or reject.
211
+ * @param approved - true to approve, false to reject.
212
+ * @returns Server response.
213
+ */
214
+ async approve(threadId, approved) {
215
+ const params = new URLSearchParams({
216
+ thread_id: threadId,
217
+ approved: String(approved).toLowerCase()
218
+ });
219
+ return this._postParams("/v2/agent/approve", params, TIMEOUTS.default);
220
+ }
221
+ /**
222
+ * POST /v2/agent/cancel — cancel a running session.
223
+ *
224
+ * Note: uses QUERY PARAMETER (thread_id), not request body.
225
+ *
226
+ * @param threadId - Graph thread ID to cancel.
227
+ * @returns Server response.
228
+ */
229
+ async cancel(threadId) {
230
+ const params = new URLSearchParams({ thread_id: threadId });
231
+ return this._postParams("/v2/agent/cancel", params, TIMEOUTS.default);
232
+ }
233
+ // -------------------------------------------------------------------------
234
+ // Internal helpers
235
+ // -------------------------------------------------------------------------
236
+ /** Shared SSE streaming implementation used by stream() and streamV3(). */
237
+ async *_sseStream(url, body, label, externalSignal) {
238
+ const timeoutSignal = AbortSignal.timeout(TIMEOUTS.read);
239
+ const signal = externalSignal ? AbortSignal.any([externalSignal, timeoutSignal]) : timeoutSignal;
240
+ let response;
241
+ try {
242
+ response = await fetch(url, {
243
+ method: "POST",
244
+ headers: { "Content-Type": "application/json" },
245
+ body: JSON.stringify(body),
246
+ signal
247
+ });
248
+ } catch (err) {
249
+ const msg = err instanceof Error ? err.message : String(err);
250
+ if (msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND") || msg.includes("fetch failed") || msg.includes("connect")) {
251
+ throw new Error(`daemon is not running. (${this._baseUrl})`);
252
+ }
253
+ throw err;
254
+ }
255
+ if (!response.ok) {
256
+ throw new Error(`[${label}] HTTP ${response.status}: ${url}`);
257
+ }
258
+ if (!response.body) {
259
+ throw new Error(`[${label}] Response body is null: ${url}`);
260
+ }
261
+ const sseStream = response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream());
262
+ for await (const event of sseStream) {
263
+ if (event.data) {
264
+ try {
265
+ yield JSON.parse(event.data);
266
+ } catch {
267
+ process.stderr.write(`[${label}] SSE JSON parse failed: len=${event.data.length}
268
+ `);
269
+ }
270
+ }
271
+ }
272
+ }
273
+ /** GET helper with connection-error normalisation. */
274
+ async _get(path, timeoutMs) {
275
+ const url = `${this._baseUrl}${path}`;
276
+ let response;
277
+ try {
278
+ response = await fetch(url, { signal: makeSignal(timeoutMs) });
279
+ } catch (err) {
280
+ const msg = err instanceof Error ? err.message : String(err);
281
+ if (msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND") || msg.includes("fetch failed") || msg.includes("connect")) {
282
+ throw new Error(`daemon is not running. (${this._baseUrl})`);
283
+ }
284
+ throw err;
285
+ }
286
+ if (!response.ok) {
287
+ throw new Error(`[http_client] HTTP ${response.status}: ${url}`);
288
+ }
289
+ return response.json();
290
+ }
291
+ /** POST with JSON body helper. */
292
+ async _post(path, body, timeoutMs, externalSignal) {
293
+ const url = `${this._baseUrl}${path}`;
294
+ const timeoutSignal = makeSignal(timeoutMs);
295
+ const signal = externalSignal ? AbortSignal.any([externalSignal, timeoutSignal]) : timeoutSignal;
296
+ let response;
297
+ try {
298
+ response = await fetch(url, {
299
+ method: "POST",
300
+ headers: { "Content-Type": "application/json" },
301
+ body: JSON.stringify(body),
302
+ signal
303
+ });
304
+ } catch (err) {
305
+ const msg = err instanceof Error ? err.message : String(err);
306
+ if (msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND") || msg.includes("fetch failed") || msg.includes("connect")) {
307
+ throw new Error(`daemon is not running. (${this._baseUrl})`);
308
+ }
309
+ throw err;
310
+ }
311
+ if (!response.ok) {
312
+ throw new Error(`[http_client] HTTP ${response.status}: ${url}`);
313
+ }
314
+ return response.json();
315
+ }
316
+ /** POST with query-parameter helper (used by /approve and /cancel). */
317
+ async _postParams(path, params, timeoutMs) {
318
+ const url = `${this._baseUrl}${path}?${params.toString()}`;
319
+ let response;
320
+ try {
321
+ response = await fetch(url, {
322
+ method: "POST",
323
+ signal: makeSignal(timeoutMs)
324
+ });
325
+ } catch (err) {
326
+ const msg = err instanceof Error ? err.message : String(err);
327
+ if (msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND") || msg.includes("fetch failed") || msg.includes("connect")) {
328
+ throw new Error(`daemon is not running. (${this._baseUrl})`);
329
+ }
330
+ throw err;
331
+ }
332
+ if (!response.ok) {
333
+ throw new Error(`[http_client] HTTP ${response.status}: ${url}`);
334
+ }
335
+ return response.json();
336
+ }
337
+ };
338
+ function sleep(ms) {
339
+ return new Promise((resolve) => setTimeout(resolve, ms));
340
+ }
341
+
342
+ // src/hooks/useDaemon.ts
343
+ import { useState, useEffect, useRef } from "react";
344
+ function useDaemon(client) {
345
+ const [state, setState] = useState({
346
+ ready: false,
347
+ waiting: true,
348
+ error: null
349
+ });
350
+ const controllerRef = useRef(new AbortController());
351
+ useEffect(() => {
352
+ let mounted = true;
353
+ async function check() {
354
+ try {
355
+ await client.health();
356
+ if (mounted) setState({ ready: true, waiting: false, error: null });
357
+ } catch {
358
+ if (mounted) setState((s) => ({ ...s, waiting: true }));
359
+ try {
360
+ const ok = await client.waitForReady();
361
+ if (mounted) {
362
+ setState({
363
+ ready: ok,
364
+ waiting: false,
365
+ error: ok ? null : "Server connection timed out"
366
+ });
367
+ }
368
+ } catch (err) {
369
+ if (mounted) {
370
+ setState({
371
+ ready: false,
372
+ waiting: false,
373
+ error: err instanceof Error ? err.message : String(err)
374
+ });
375
+ }
376
+ }
377
+ }
378
+ }
379
+ check();
380
+ return () => {
381
+ mounted = false;
382
+ controllerRef.current.abort();
383
+ };
384
+ }, [client]);
385
+ return state;
386
+ }
387
+
388
+ // src/hooks/useSSE.ts
389
+ import { useCallback, useEffect as useEffect2, useRef as useRef2 } from "react";
390
+
391
+ // src/types.ts
392
+ var NODE_STATUS_MESSAGES = {
393
+ session_load: "\uC138\uC158 \uB85C\uB4DC \uC911\u2026",
394
+ agent: "\uC5D0\uC774\uC804\uD2B8 \uCD94\uB860 \uC911\u2026",
395
+ approval_wait: "\uC2B9\uC778 \uB300\uAE30 \uC911\u2026",
396
+ tools: "\uB3C4\uAD6C \uC2E4\uD589 \uC911\u2026",
397
+ persist: "\uC800\uC7A5 \uC911\u2026"
398
+ };
399
+ var TASK_TYPE_LABELS = {
400
+ draft_response: "\uB2F5\uBCC0 \uCD08\uC548 \uC791\uC131",
401
+ revise_response: "\uB2F5\uBCC0 \uC218\uC815",
402
+ lookup_stats: "\uD1B5\uACC4 \uC870\uD68C",
403
+ issue_detection: "\uC774\uC288 \uD0D0\uC9C0",
404
+ stats_query: "\uD1B5\uACC4 \uC870\uD68C",
405
+ keyword_analysis: "\uD0A4\uC6CC\uB4DC \uBD84\uC11D",
406
+ demographics_query: "\uC778\uAD6C\uD1B5\uACC4 \uC870\uD68C",
407
+ default: "\uC77C\uBC18 \uC791\uC5C5"
408
+ };
409
+ var TASK_TYPE_STYLES = {
410
+ draft_response: "cyan",
411
+ revise_response: "blue",
412
+ lookup_stats: "magenta",
413
+ issue_detection: "yellow",
414
+ stats_query: "magenta",
415
+ keyword_analysis: "yellow",
416
+ demographics_query: "blueBright",
417
+ default: "cyan"
418
+ };
419
+ var TOOL_DISPLAY_NAMES = {
420
+ stats_lookup: "\uBBFC\uC6D0 \uD1B5\uACC4",
421
+ keyword_analyzer: "\uD0A4\uC6CC\uB4DC \uBD84\uC11D",
422
+ demographics_lookup: "\uC778\uAD6C\uD1B5\uACC4"
423
+ };
424
+
425
+ // src/hooks/useSSE.ts
426
+ function useSSE({ client, dispatch, sessionId }) {
427
+ const abortRef = useRef2(null);
428
+ const mountedRef = useRef2(true);
429
+ useEffect2(() => {
430
+ mountedRef.current = true;
431
+ return () => {
432
+ mountedRef.current = false;
433
+ };
434
+ }, []);
435
+ const safeDispatch = useCallback(
436
+ (action) => {
437
+ if (mountedRef.current) dispatch(action);
438
+ },
439
+ [dispatch]
440
+ );
441
+ const cancel = useCallback(() => {
442
+ if (abortRef.current) {
443
+ abortRef.current.abort();
444
+ abortRef.current = null;
445
+ }
446
+ }, []);
447
+ const submit = useCallback(
448
+ async (query2) => {
449
+ cancel();
450
+ const controller = new AbortController();
451
+ abortRef.current = controller;
452
+ const isCurrentRun = () => abortRef.current === controller;
453
+ const assistantMsgId = crypto.randomUUID();
454
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
455
+ const sid = sessionId ?? void 0;
456
+ safeDispatch({ type: "SET_LOADING", payload: true });
457
+ safeDispatch({ type: "SET_ERROR", payload: null });
458
+ safeDispatch({
459
+ type: "START_ASSISTANT_MESSAGE",
460
+ payload: { id: assistantMsgId, timestamp }
461
+ });
462
+ const { success: v3Success, contentDispatched: v3ContentDispatched } = await _tryV3(
463
+ client,
464
+ query2,
465
+ sid,
466
+ assistantMsgId,
467
+ controller,
468
+ safeDispatch
469
+ );
470
+ if (v3Success) {
471
+ safeDispatch({ type: "SET_API_VERSION", payload: "v3" });
472
+ safeDispatch({ type: "SET_LOADING", payload: false });
473
+ if (isCurrentRun()) abortRef.current = null;
474
+ return;
475
+ }
476
+ if (controller.signal.aborted) {
477
+ safeDispatch({ type: "SET_LOADING", payload: false });
478
+ if (isCurrentRun()) abortRef.current = null;
479
+ return;
480
+ }
481
+ if (v3ContentDispatched) {
482
+ safeDispatch({
483
+ type: "SET_ERROR",
484
+ payload: "Streaming interrupted after partial response \u2014 please retry."
485
+ });
486
+ safeDispatch({ type: "SET_LOADING", payload: false });
487
+ if (isCurrentRun()) abortRef.current = null;
488
+ return;
489
+ }
490
+ const v2Success = await _tryV2(
491
+ client,
492
+ query2,
493
+ sid,
494
+ assistantMsgId,
495
+ controller,
496
+ safeDispatch
497
+ );
498
+ if (v2Success) {
499
+ safeDispatch({ type: "SET_API_VERSION", payload: "v2" });
500
+ safeDispatch({ type: "SET_LOADING", payload: false });
501
+ if (isCurrentRun()) abortRef.current = null;
502
+ return;
503
+ }
504
+ if (controller.signal.aborted) {
505
+ safeDispatch({ type: "SET_LOADING", payload: false });
506
+ if (isCurrentRun()) abortRef.current = null;
507
+ return;
508
+ }
509
+ await _tryBlocking(client, query2, sid, assistantMsgId, controller, safeDispatch);
510
+ safeDispatch({ type: "SET_LOADING", payload: false });
511
+ if (isCurrentRun()) abortRef.current = null;
512
+ },
513
+ [client, safeDispatch, sessionId, cancel]
514
+ );
515
+ return { submit, cancel };
516
+ }
517
+ async function _tryV3(client, query2, sessionId, assistantMsgId, controller, dispatch) {
518
+ let contentDispatched = false;
519
+ try {
520
+ for await (const raw of client.streamV3(query2, sessionId, void 0, controller.signal)) {
521
+ if (controller.signal.aborted) return { success: false, contentDispatched };
522
+ const event = raw;
523
+ switch (event.type) {
524
+ case "thinking_start":
525
+ dispatch({
526
+ type: "START_THINKING_STEP",
527
+ payload: { iteration: event.iteration }
528
+ });
529
+ break;
530
+ case "thinking_delta":
531
+ contentDispatched = true;
532
+ dispatch({
533
+ type: "APPEND_STREAMING_THINKING",
534
+ payload: event.content
535
+ });
536
+ break;
537
+ case "thinking_end":
538
+ dispatch({
539
+ type: "END_THINKING_STEP",
540
+ payload: { iteration: event.iteration, tool_calls: event.tool_calls }
541
+ });
542
+ break;
543
+ case "tool_start":
544
+ dispatch({
545
+ type: "MARK_TOOL_START",
546
+ payload: { tool: event.tool }
547
+ });
548
+ break;
549
+ case "tool_end":
550
+ dispatch({
551
+ type: "MARK_TOOL_END",
552
+ payload: { tool: event.tool, success: event.success }
553
+ });
554
+ break;
555
+ case "response_delta":
556
+ contentDispatched = true;
557
+ dispatch({
558
+ type: "APPEND_STREAMING_CONTENT",
559
+ payload: event.content
560
+ });
561
+ break;
562
+ case "run_complete":
563
+ dispatch({
564
+ type: "FINALIZE_ASSISTANT_MESSAGE",
565
+ payload: {
566
+ messageId: assistantMsgId,
567
+ content: event.text,
568
+ evidence: event.evidence_items,
569
+ metadata: event.metadata,
570
+ sessionId: event.session_id,
571
+ threadId: event.thread_id
572
+ }
573
+ });
574
+ return { success: true, contentDispatched };
575
+ case "error":
576
+ dispatch({ type: "SET_ERROR", payload: event.error });
577
+ return { success: false, contentDispatched: true };
578
+ default:
579
+ break;
580
+ }
581
+ }
582
+ return { success: false, contentDispatched };
583
+ } catch (_err) {
584
+ return { success: false, contentDispatched };
585
+ }
586
+ }
587
+ async function _tryV2(client, query2, sessionId, assistantMsgId, controller, dispatch) {
588
+ try {
589
+ for await (const raw of client.stream(query2, sessionId, controller.signal)) {
590
+ if (controller.signal.aborted) return false;
591
+ const event = raw;
592
+ const { node, status } = event;
593
+ const statusMsg = NODE_STATUS_MESSAGES[node];
594
+ if (statusMsg !== void 0) {
595
+ dispatch({ type: "SET_STATUS_LABEL", payload: statusMsg });
596
+ }
597
+ if (node === "agent" && status === "completed") {
598
+ if (event.planned_tools && event.planned_tools.length > 0) {
599
+ dispatch({
600
+ type: "SET_STATUS_LABEL",
601
+ payload: `\uB3C4\uAD6C \uC2E4\uD589 \uC608\uC815: ${event.planned_tools.join(", ")}`
602
+ });
603
+ }
604
+ if (event.final_text !== void 0) {
605
+ dispatch({
606
+ type: "FINALIZE_ASSISTANT_MESSAGE",
607
+ payload: {
608
+ messageId: assistantMsgId,
609
+ content: event.final_text,
610
+ evidence: event.evidence_items ?? [],
611
+ metadata: {},
612
+ sessionId: event.session_id ?? "",
613
+ threadId: event.thread_id ?? ""
614
+ }
615
+ });
616
+ return true;
617
+ }
618
+ }
619
+ if (node === "tools" && status === "completed") {
620
+ dispatch({ type: "SET_STATUS_LABEL", payload: "\uB3C4\uAD6C \uC2E4\uD589 \uC644\uB8CC" });
621
+ }
622
+ if (node === "approval_wait" && status === "awaiting_approval") {
623
+ if (event.approval_request !== void 0) {
624
+ dispatch({
625
+ type: "SET_PENDING_APPROVAL",
626
+ payload: event.approval_request
627
+ });
628
+ }
629
+ }
630
+ if (node === "persist" && status === "completed") {
631
+ if (event.final_text !== void 0) {
632
+ dispatch({
633
+ type: "FINALIZE_ASSISTANT_MESSAGE",
634
+ payload: {
635
+ messageId: assistantMsgId,
636
+ content: event.final_text,
637
+ evidence: event.evidence_items ?? [],
638
+ metadata: {},
639
+ sessionId: event.session_id ?? "",
640
+ threadId: event.thread_id ?? ""
641
+ }
642
+ });
643
+ return true;
644
+ }
645
+ }
646
+ if (node === "error") {
647
+ dispatch({
648
+ type: "SET_ERROR",
649
+ payload: event.error ?? "Unknown error from v2 stream"
650
+ });
651
+ return false;
652
+ }
653
+ }
654
+ return false;
655
+ } catch (_err) {
656
+ return false;
657
+ }
658
+ }
659
+ async function _tryBlocking(client, query2, sessionId, assistantMsgId, controller, dispatch) {
660
+ if (controller.signal.aborted) return;
661
+ try {
662
+ const result = await client.run(query2, sessionId, controller.signal);
663
+ if (controller.signal.aborted) return;
664
+ dispatch({
665
+ type: "FINALIZE_ASSISTANT_MESSAGE",
666
+ payload: {
667
+ messageId: assistantMsgId,
668
+ content: result.text,
669
+ evidence: result.evidence_items ?? [],
670
+ metadata: result.metadata ?? {},
671
+ sessionId: result.session_id,
672
+ threadId: result.thread_id ?? ""
673
+ }
674
+ });
675
+ dispatch({ type: "SET_API_VERSION", payload: "v2" });
676
+ } catch (err) {
677
+ if (controller.signal.aborted) return;
678
+ const msg = err instanceof Error ? err.message : String(err);
679
+ dispatch({ type: "SET_ERROR", payload: msg });
680
+ }
681
+ }
682
+
683
+ // src/hooks/useHistory.ts
684
+ import { useReducer, useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3 } from "react";
685
+ import { writeFile } from "node:fs/promises";
686
+ import { readFileSync, mkdirSync, lstatSync } from "node:fs";
687
+ import { join } from "node:path";
688
+ import { homedir } from "node:os";
689
+ var HISTORY_DIR = join(homedir(), ".govon");
690
+ var HISTORY_FILE = join(HISTORY_DIR, "history");
691
+ var MAX_ENTRIES = 500;
692
+ var MAX_ENTRY_BYTES = 4096;
693
+ var DEBOUNCE_MS = 300;
694
+ function historyReducer(state, action) {
695
+ switch (action.type) {
696
+ case "PUSH": {
697
+ const trimmed = action.entry.trim();
698
+ if (!trimmed) return state;
699
+ if (Buffer.byteLength(trimmed, "utf8") > MAX_ENTRY_BYTES) return state;
700
+ const next = state.entries[state.entries.length - 1] === trimmed ? state.entries : [...state.entries, trimmed];
701
+ return { entries: next.slice(-MAX_ENTRIES), cursor: -1 };
702
+ }
703
+ case "NAVIGATE": {
704
+ const { entries, cursor } = state;
705
+ if (entries.length === 0) return state;
706
+ if (action.direction === "up") {
707
+ const next = cursor === -1 ? entries.length - 1 : Math.max(0, cursor - 1);
708
+ return { ...state, cursor: next };
709
+ }
710
+ if (cursor === -1) return state;
711
+ if (cursor + 1 >= entries.length) {
712
+ return { ...state, cursor: -1 };
713
+ }
714
+ return { ...state, cursor: cursor + 1 };
715
+ }
716
+ case "RESET_CURSOR":
717
+ return { ...state, cursor: -1 };
718
+ default:
719
+ return state;
720
+ }
721
+ }
722
+ function loadInitialEntries() {
723
+ try {
724
+ const raw = readFileSync(HISTORY_FILE, "utf-8");
725
+ return raw.split("\n").filter(Boolean).slice(-MAX_ENTRIES);
726
+ } catch {
727
+ return [];
728
+ }
729
+ }
730
+ async function persistEntries(entries) {
731
+ try {
732
+ mkdirSync(HISTORY_DIR, { recursive: true });
733
+ try {
734
+ const stat = lstatSync(HISTORY_FILE);
735
+ if (!stat.isFile()) return;
736
+ } catch {
737
+ }
738
+ await writeFile(HISTORY_FILE, entries.join("\n") + "\n", "utf-8");
739
+ } catch {
740
+ }
741
+ }
742
+ function useHistory() {
743
+ const [state, dispatch] = useReducer(historyReducer, void 0, () => ({
744
+ entries: loadInitialEntries(),
745
+ cursor: -1
746
+ }));
747
+ const { entries, cursor } = state;
748
+ const debounceTimerRef = useRef3(null);
749
+ const writeInProgressRef = useRef3(false);
750
+ const pendingWriteRef = useRef3(false);
751
+ const latestEntriesRef = useRef3(entries);
752
+ latestEntriesRef.current = entries;
753
+ useEffect3(() => {
754
+ if (debounceTimerRef.current !== null) {
755
+ clearTimeout(debounceTimerRef.current);
756
+ }
757
+ debounceTimerRef.current = setTimeout(() => {
758
+ debounceTimerRef.current = null;
759
+ if (writeInProgressRef.current) {
760
+ pendingWriteRef.current = true;
761
+ return;
762
+ }
763
+ writeInProgressRef.current = true;
764
+ persistEntries(latestEntriesRef.current).finally(() => {
765
+ writeInProgressRef.current = false;
766
+ if (pendingWriteRef.current) {
767
+ pendingWriteRef.current = false;
768
+ writeInProgressRef.current = true;
769
+ void persistEntries(latestEntriesRef.current).finally(() => {
770
+ writeInProgressRef.current = false;
771
+ });
772
+ }
773
+ });
774
+ }, DEBOUNCE_MS);
775
+ return () => {
776
+ if (debounceTimerRef.current !== null) {
777
+ clearTimeout(debounceTimerRef.current);
778
+ debounceTimerRef.current = null;
779
+ }
780
+ };
781
+ }, [entries]);
782
+ const push = useCallback2((entry) => {
783
+ dispatch({ type: "PUSH", entry });
784
+ }, []);
785
+ const navigate = useCallback2(
786
+ (direction) => {
787
+ if (entries.length === 0) return void 0;
788
+ const prevCursor = cursor;
789
+ if (direction === "down") {
790
+ if (prevCursor === -1) return void 0;
791
+ if (prevCursor + 1 >= entries.length) {
792
+ dispatch({ type: "NAVIGATE", direction: "down" });
793
+ return "";
794
+ }
795
+ }
796
+ dispatch({ type: "NAVIGATE", direction });
797
+ if (direction === "up") {
798
+ const next = prevCursor === -1 ? entries.length - 1 : Math.max(0, prevCursor - 1);
799
+ return entries[next];
800
+ } else {
801
+ return entries[prevCursor + 1];
802
+ }
803
+ },
804
+ [entries, cursor]
805
+ );
806
+ const resetCursor = useCallback2(() => {
807
+ dispatch({ type: "RESET_CURSOR" });
808
+ }, []);
809
+ return { entries, push, navigate, resetCursor, cursor };
810
+ }
811
+
812
+ // src/components/Banner.tsx
813
+ import { Box, Text } from "ink";
814
+ import { jsx, jsxs } from "react/jsx-runtime";
815
+ var LOGO_ART = [
816
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
817
+ " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D ",
818
+ " \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2557",
819
+ " \u2588\u2588\u2551 \u255A\u2550\u2588\u2588\u2551",
820
+ " \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
821
+ " \u255A\u2550\u2550\u2550\u2550\u2550\u255D "
822
+ ];
823
+ function Banner({ version }) {
824
+ const baseUrl = getBaseUrl();
825
+ let modeLabel;
826
+ try {
827
+ const url = new URL(baseUrl);
828
+ const isLocal = url.hostname === "127.0.0.1" || url.hostname === "localhost";
829
+ modeLabel = isLocal ? "\uB85C\uCEEC \uBAA8\uB4DC" : `\uC6D0\uACA9: ${url.hostname}`;
830
+ } catch {
831
+ modeLabel = "\uB85C\uCEEC \uBAA8\uB4DC";
832
+ }
833
+ return /* @__PURE__ */ jsxs(
834
+ Box,
835
+ {
836
+ borderStyle: "round",
837
+ borderColor: THEME_COLORS.primary,
838
+ paddingX: 2,
839
+ paddingY: 1,
840
+ flexDirection: "row",
841
+ children: [
842
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginRight: 3, children: [
843
+ LOGO_ART.map((line, i) => /* @__PURE__ */ jsx(Text, { color: THEME_COLORS.accent, children: line }, i)),
844
+ /* @__PURE__ */ jsx(Text, { children: " " }),
845
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: THEME_COLORS.accent, children: [
846
+ "GovOn v",
847
+ version
848
+ ] }),
849
+ /* @__PURE__ */ jsx(Text, { color: THEME_COLORS.muted, children: modeLabel })
850
+ ] }),
851
+ /* @__PURE__ */ jsx(
852
+ Box,
853
+ {
854
+ borderStyle: "single",
855
+ borderLeft: true,
856
+ borderRight: false,
857
+ borderTop: false,
858
+ borderBottom: false,
859
+ borderColor: THEME_COLORS.primary,
860
+ marginRight: 3
861
+ }
862
+ ),
863
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
864
+ /* @__PURE__ */ jsx(Text, { bold: true, color: THEME_COLORS.accent, children: "\uC2DC\uC791 \uAC00\uC774\uB4DC" }),
865
+ /* @__PURE__ */ jsx(Text, { color: THEME_COLORS.muted, children: "\uC9C8\uBB38\uC744 \uC785\uB825\uD558\uBA74 AI \uC5D0\uC774\uC804\uD2B8\uAC00 \uBD84\uC11D\uD558\uACE0 \uB3C4\uAD6C\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4" }),
866
+ /* @__PURE__ */ jsx(Text, { color: THEME_COLORS.muted, children: "/help \uB85C \uBA85\uB839\uC5B4 \uBAA9\uB85D \uD655\uC778" }),
867
+ /* @__PURE__ */ jsx(Text, { color: THEME_COLORS.muted, children: "/exit \uB610\uB294 Ctrl+D \uB85C \uC885\uB8CC" }),
868
+ /* @__PURE__ */ jsx(Text, { children: " " }),
869
+ /* @__PURE__ */ jsx(Text, { color: THEME_COLORS.muted, children: "\uC2B9\uC778 \uD544\uC694 \uB3C4\uAD6C\uB294 \uC2E4\uD589 \uC804 \uD655\uC778\uC744 \uC694\uCCAD\uD569\uB2C8\uB2E4" }),
870
+ /* @__PURE__ */ jsx(Text, { color: THEME_COLORS.muted, children: "Esc \uB85C \uC9C4\uD589 \uC911\uC778 \uC791\uC5C5 \uCDE8\uC18C" })
871
+ ] })
872
+ ]
873
+ }
874
+ );
875
+ }
876
+
877
+ // src/components/InputBar.tsx
878
+ import { useState as useState2, useCallback as useCallback3, useEffect as useEffect4 } from "react";
879
+ import { Box as Box2, Text as Text2 } from "ink";
880
+ import TextInput from "ink-text-input";
881
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
882
+ function InputBar({ onSubmit, disabled = false }) {
883
+ const [value, setValue] = useState2("");
884
+ useEffect4(() => {
885
+ if (disabled) setValue("");
886
+ }, [disabled]);
887
+ const handleChange = useCallback3(
888
+ (next) => {
889
+ if (!disabled) setValue(next);
890
+ },
891
+ [disabled]
892
+ );
893
+ const handleSubmit = useCallback3(
894
+ (val) => {
895
+ const trimmed = val.trim();
896
+ if (!trimmed || disabled) return;
897
+ onSubmit(trimmed);
898
+ setValue("");
899
+ },
900
+ [onSubmit, disabled]
901
+ );
902
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
903
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: disabled ? "gray" : "green", children: "\u276F " }),
904
+ /* @__PURE__ */ jsx2(
905
+ TextInput,
906
+ {
907
+ value,
908
+ onChange: handleChange,
909
+ onSubmit: handleSubmit,
910
+ placeholder: disabled ? "\uCC98\uB9AC \uC911\u2026" : "\uC9C8\uBB38\uC744 \uC785\uB825\uD558\uC138\uC694"
911
+ }
912
+ )
913
+ ] });
914
+ }
915
+
916
+ // src/components/Spinner.tsx
917
+ import { useState as useState3, useEffect as useEffect5, useRef as useRef4 } from "react";
918
+ import { Text as Text3 } from "ink";
919
+
920
+ // src/data/spinnerVerbs.ts
921
+ var SPINNER_VERBS = [
922
+ // 사자성어 (Four-character idioms)
923
+ "\uC628\uACE0\uC9C0\uC2E0 (\u6EAB\u6545\u77E5\u65B0)",
924
+ "\uC720\uBE44\uBB34\uD658 (\u6709\u5099\u7121\u60A3)",
925
+ "\uC77C\uC11D\uC774\uC870 (\u4E00\u77F3\u4E8C\u9CE5)",
926
+ "\uB300\uAE30\uB9CC\uC131 (\u5927\u5668\u6669\u6210)",
927
+ "\uC790\uAC15\uBD88\uC2DD (\u81EA\u5F37\u4E0D\u606F)",
928
+ "\uD615\uC124\uC9C0\uACF5 (\u87A2\u96EA\u4E4B\u529F)",
929
+ "\uC6B0\uACF5\uC774\uC0B0 (\u611A\u516C\u79FB\u5C71)",
930
+ "\uB9C8\uBD80\uC704\uCE68 (\u78E8\u65A7\u7232\u91DD)",
931
+ "\uACAC\uC778\uBD88\uBC1C (\u5805\u5FCD\u4E0D\u62D4)",
932
+ "\uAE08\uC0C1\uCCA8\uD654 (\u9326\u4E0A\u6DFB\u82B1)",
933
+ "\uC120\uACAC\uC9C0\uBA85 (\u5148\u898B\u4E4B\u660E)",
934
+ "\uD0C0\uC0B0\uC9C0\uC11D (\u4ED6\u5C71\u4E4B\u77F3)",
935
+ "\uD654\uB8E1\uC810\uC815 (\u756B\u9F8D\u9EDE\u775B)",
936
+ "\uC0AC\uD544\uADC0\uC815 (\u4E8B\u5FC5\u6B78\u6B63)",
937
+ "\uC804\uD654\uC704\uBCF5 (\u8F49\u798D\u7232\u798F)",
938
+ "\uACE0\uC9C4\uAC10\uB798 (\u82E6\u76E1\u7518\u4F86)",
939
+ "\uBD88\uCCA0\uC8FC\uC57C (\u4E0D\u64A4\u665D\u591C)",
940
+ "\uC808\uCC28\uD0C1\uB9C8 (\u5207\u78CB\u7422\u78E8)",
941
+ "\uC2EC\uC0AC\uC219\uACE0 (\u6DF1\u601D\u719F\u8003)",
942
+ "\uCC9C\uC7AC\uC77C\uC6B0 (\u5343\u8F09\u4E00\u9047)",
943
+ "\uC774\uC2EC\uC804\uC2EC (\u4EE5\u5FC3\u50B3\u5FC3)",
944
+ "\uBC31\uBB38\uBD88\uC5EC\uC77C\uACAC (\u767E\u805E\u4E0D\u5982\u4E00\u898B)",
945
+ "\uAC01\uACE8\uB09C\uB9DD (\u523B\u9AA8\u96E3\u5FD8)",
946
+ "\uACB0\uCD08\uBCF4\uC740 (\u7D50\u8349\u5831\u6069)",
947
+ "\uAC1C\uACFC\uCC9C\uC120 (\u6539\u904E\u9077\u5584)",
948
+ // 속담 (Proverbs)
949
+ "\uCC9C \uB9AC \uAE38\uB3C4 \uD55C \uAC78\uC74C\uBD80\uD130",
950
+ "\uAD6C\uC2AC\uC774 \uC11C \uB9D0\uC774\uB77C\uB3C4 \uAFF0\uC5B4\uC57C \uBCF4\uBC30",
951
+ "\uB3CC\uB2E4\uB9AC\uB3C4 \uB450\uB4E4\uACA8 \uBCF4\uACE0 \uAC74\uB108\uB77C",
952
+ "\uB73B\uC774 \uC788\uB294 \uACF3\uC5D0 \uAE38\uC774 \uC788\uB2E4",
953
+ "\uACF5\uB4E0 \uD0D1\uC774 \uBB34\uB108\uC9C0\uB7B4",
954
+ "\uD2F0\uB04C \uBAA8\uC544 \uD0DC\uC0B0",
955
+ "\uD558\uB298\uC740 \uC2A4\uC2A4\uB85C \uB3D5\uB294 \uC790\uB97C \uB3D5\uB294\uB2E4",
956
+ "\uC2E4\uD328\uB294 \uC131\uACF5\uC758 \uC5B4\uBA38\uB2C8",
957
+ "\uC138 \uC0B4 \uBC84\uB987 \uC5EC\uB4E0\uAE4C\uC9C0 \uAC04\uB2E4",
958
+ "\uBE48 \uC218\uB808\uAC00 \uC694\uB780\uD558\uB2E4",
959
+ "\uB4F1\uC794 \uBC11\uC774 \uC5B4\uB461\uB2E4",
960
+ "\uD638\uB791\uC774\uB3C4 \uC81C \uB9D0 \uD558\uBA74 \uC628\uB2E4",
961
+ "\uC6D0\uC22D\uC774\uB3C4 \uB098\uBB34\uC5D0\uC11C \uB5A8\uC5B4\uC9C4\uB2E4",
962
+ "\uC18C \uC783\uACE0 \uC678\uC591\uAC04 \uACE0\uCE5C\uB2E4",
963
+ "\uAC00\uB294 \uB9D0\uC774 \uACE0\uC640\uC57C \uC624\uB294 \uB9D0\uC774 \uACF1\uB2E4",
964
+ // 재미있는 상식 (Fun facts)
965
+ "\uAFC0\uBC8C\uC740 \uD55C \uC21F\uAC08\uC758 \uAFC0\uC744 \uC704\uD574 \uD3C9\uC0DD \uB09C\uB2E4",
966
+ "\uBB38\uC5B4\uC758 \uC2EC\uC7A5\uC740 \uC138 \uAC1C\uB2E4",
967
+ "\uBC14\uB098\uB098\uB294 \uC2DD\uBB3C\uD559\uC801\uC73C\uB85C \uC7A5\uACFC(berry)\uC774\uB2E4",
968
+ "\uC9C0\uAD6C\uC5D0\uC11C \uAC00\uC7A5 \uC624\uB798\uB41C \uB098\uBB34\uB294 5000\uC0B4\uC774 \uB118\uB294\uB2E4",
969
+ "\uC778\uAC04\uC758 \uB1CC\uB294 60%\uAC00 \uC9C0\uBC29\uC774\uB2E4",
970
+ "\uD574\uD30C\uB9AC\uB294 \uB1CC\uAC00 \uC5C6\uB2E4",
971
+ "\uB099\uD0C0\uC758 \uD639\uC5D0\uB294 \uBB3C\uC774 \uC544\uB2C8\uB77C \uC9C0\uBC29\uC774 \uC788\uB2E4"
972
+ ];
973
+
974
+ // src/components/Spinner.tsx
975
+ import { jsxs as jsxs3 } from "react/jsx-runtime";
976
+ function formatElapsed(seconds) {
977
+ if (seconds < 60) return `${seconds}s`;
978
+ const m = Math.floor(seconds / 60);
979
+ const s = seconds % 60;
980
+ return `${m}m ${String(s).padStart(2, "0")}s`;
981
+ }
982
+ function formatTokens(count) {
983
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
984
+ return String(count);
985
+ }
986
+ function randomVerb() {
987
+ return SPINNER_VERBS[Math.floor(Math.random() * SPINNER_VERBS.length)];
988
+ }
989
+ function Spinner({ tokens = 0 }) {
990
+ const [frame, setFrame] = useState3(0);
991
+ const [verb, setVerb] = useState3(randomVerb);
992
+ const tickRef = useRef4(0);
993
+ const startTime = useRef4(Date.now());
994
+ useEffect5(() => {
995
+ const interval = setInterval(() => {
996
+ setFrame((f) => f + 1);
997
+ tickRef.current += 1;
998
+ if (tickRef.current % SPINNER.verbChangeInterval === 0) {
999
+ setVerb(randomVerb());
1000
+ }
1001
+ }, 1e3 / SPINNER.fps);
1002
+ return () => clearInterval(interval);
1003
+ }, []);
1004
+ const char = SPINNER.chars[frame % SPINNER.chars.length];
1005
+ const elapsed = Math.floor((Date.now() - startTime.current) / 1e3);
1006
+ const elapsedStr = formatElapsed(elapsed);
1007
+ const suffix = tokens > 0 ? ` (${elapsedStr} \xB7 \u2193 ${formatTokens(tokens)} tokens)` : ` (${elapsedStr})`;
1008
+ return /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1009
+ char,
1010
+ " ",
1011
+ verb,
1012
+ "\u2026",
1013
+ suffix
1014
+ ] });
1015
+ }
1016
+
1017
+ // src/components/ThinkingBlock.tsx
1018
+ import { Box as Box3, Text as Text4 } from "ink";
1019
+ import { jsx as jsx3, jsxs as jsxs4 } from "react/jsx-runtime";
1020
+ function ThinkingBlock({ content, streaming = false }) {
1021
+ if (!content) return null;
1022
+ return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", marginLeft: 2, children: [
1023
+ /* @__PURE__ */ jsxs4(Text4, { color: THEME_COLORS.muted, dimColor: true, children: [
1024
+ "\u{1F4AD} ",
1025
+ streaming ? "\uC0AC\uACE0 \uC911\u2026" : "\uC0AC\uACE0 \uC644\uB8CC"
1026
+ ] }),
1027
+ /* @__PURE__ */ jsx3(Box3, { marginLeft: 2, children: /* @__PURE__ */ jsx3(Text4, { color: THEME_COLORS.dimmed, dimColor: true, wrap: "wrap", children: content }) })
1028
+ ] });
1029
+ }
1030
+
1031
+ // src/components/MarkdownView.tsx
1032
+ import { useMemo } from "react";
1033
+ import { Box as Box4, Text as Text5 } from "ink";
1034
+ import { marked } from "marked";
1035
+ import { markedTerminal } from "marked-terminal";
1036
+ import { jsx as jsx4, jsxs as jsxs5 } from "react/jsx-runtime";
1037
+ marked.use(
1038
+ markedTerminal({
1039
+ reflowText: true,
1040
+ width: process.stdout.columns || 80,
1041
+ showSectionPrefix: false
1042
+ })
1043
+ );
1044
+ function stripAnsi(str) {
1045
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
1046
+ }
1047
+ function MarkdownView({ content, streaming = false }) {
1048
+ const rendered = useMemo(() => {
1049
+ if (!content) return "";
1050
+ try {
1051
+ const result = marked.parse(stripAnsi(content));
1052
+ return typeof result === "string" ? result.trimEnd() : "";
1053
+ } catch {
1054
+ return content;
1055
+ }
1056
+ }, [content]);
1057
+ if (!rendered) return null;
1058
+ return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
1059
+ /* @__PURE__ */ jsx4(Text5, { wrap: "wrap", children: rendered }),
1060
+ streaming && /* @__PURE__ */ jsx4(Text5, { dimColor: true, children: "\u258D" })
1061
+ ] });
1062
+ }
1063
+
1064
+ // src/components/MessageBubble.tsx
1065
+ import { Box as Box7, Text as Text8 } from "ink";
1066
+
1067
+ // src/components/ToolPanel.tsx
1068
+ import { Box as Box5, Text as Text6 } from "ink";
1069
+ import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
1070
+ function ToolPanel({ tools }) {
1071
+ if (tools.length === 0) return null;
1072
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginLeft: 2, children: tools.map((tool, i) => {
1073
+ const displayName = TOOL_DISPLAY_NAMES[tool.tool] ?? tool.tool;
1074
+ if (tool.pending) {
1075
+ return /* @__PURE__ */ jsxs6(Text6, { color: THEME_COLORS.warning, children: [
1076
+ "\u250C\u2500 \u2699 ",
1077
+ tool.tool,
1078
+ " (",
1079
+ displayName,
1080
+ ") \uC2E4\uD589 \uC911\u2026"
1081
+ ] }, i);
1082
+ }
1083
+ const statusColor = tool.success !== false ? THEME_COLORS.success : THEME_COLORS.error;
1084
+ const statusIcon = tool.success !== false ? "\u2726" : "\u2718";
1085
+ const statusText = tool.success !== false ? "\uC644\uB8CC" : "\uC2E4\uD328";
1086
+ return /* @__PURE__ */ jsxs6(Text6, { color: statusColor, children: [
1087
+ "\u2514\u2500 ",
1088
+ statusIcon,
1089
+ " ",
1090
+ tool.tool,
1091
+ " (",
1092
+ displayName,
1093
+ ") ",
1094
+ statusText
1095
+ ] }, i);
1096
+ }) });
1097
+ }
1098
+
1099
+ // src/components/MetadataBar.tsx
1100
+ import { Box as Box6, Text as Text7 } from "ink";
1101
+ import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
1102
+ function MetadataBar({ metadata }) {
1103
+ const parts = [];
1104
+ if (metadata.total_iterations !== void 0) {
1105
+ parts.push(`${metadata.total_iterations} iterations`);
1106
+ }
1107
+ if (metadata.total_tool_calls !== void 0) {
1108
+ parts.push(`${metadata.total_tool_calls} tools`);
1109
+ }
1110
+ if (metadata.total_latency_ms !== void 0) {
1111
+ parts.push(`${Math.round(metadata.total_latency_ms).toLocaleString()}ms`);
1112
+ }
1113
+ if (parts.length === 0) return null;
1114
+ return /* @__PURE__ */ jsx6(Box6, { marginLeft: 2, marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: THEME_COLORS.muted, dimColor: true, children: [
1115
+ "\u2500 ",
1116
+ parts.join(" \xB7 ")
1117
+ ] }) });
1118
+ }
1119
+
1120
+ // src/components/MessageBubble.tsx
1121
+ import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
1122
+ function MessageBubble({ message }) {
1123
+ if (message.role === "user") {
1124
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "row", marginTop: 1, children: [
1125
+ /* @__PURE__ */ jsx7(Text8, { bold: true, color: "green", children: "\u276F " }),
1126
+ /* @__PURE__ */ jsx7(Text8, { wrap: "wrap", children: message.content })
1127
+ ] });
1128
+ }
1129
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", marginTop: 1, children: [
1130
+ /* @__PURE__ */ jsx7(Text8, { bold: true, color: THEME_COLORS.primary, children: "GovOn" }),
1131
+ message.thinking && message.thinking.length > 0 && /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: message.thinking.map((step, i) => /* @__PURE__ */ jsx7(
1132
+ ThinkingBlock,
1133
+ {
1134
+ content: step.content,
1135
+ streaming: false
1136
+ },
1137
+ i
1138
+ )) }),
1139
+ message.tools && message.tools.length > 0 && /* @__PURE__ */ jsx7(ToolPanel, { tools: message.tools }),
1140
+ message.error ? /* @__PURE__ */ jsx7(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsxs8(Text8, { color: THEME_COLORS.error, children: [
1141
+ "\uC624\uB958: ",
1142
+ message.error
1143
+ ] }) }) : /* @__PURE__ */ jsx7(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsx7(MarkdownView, { content: message.content, streaming: false }) }),
1144
+ message.metadata && /* @__PURE__ */ jsx7(MetadataBar, { metadata: message.metadata })
1145
+ ] });
1146
+ }
1147
+
1148
+ // src/components/ApprovalPrompt.tsx
1149
+ import { useCallback as useCallback4, useRef as useRef5 } from "react";
1150
+ import { Box as Box8, Text as Text9, useInput } from "ink";
1151
+ import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
1152
+ var MAX_DESCRIPTION_LENGTH = 1e3;
1153
+ function ApprovalPrompt({ request, onApprove }) {
1154
+ const safeDescription = request.description?.slice(0, MAX_DESCRIPTION_LENGTH);
1155
+ const decidedRef = useRef5(false);
1156
+ useInput(
1157
+ useCallback4(
1158
+ (input) => {
1159
+ if (decidedRef.current) return;
1160
+ const key = input.toLowerCase();
1161
+ if (key === "y") {
1162
+ decidedRef.current = true;
1163
+ onApprove(true);
1164
+ } else if (key === "n") {
1165
+ decidedRef.current = true;
1166
+ onApprove(false);
1167
+ }
1168
+ },
1169
+ [onApprove]
1170
+ )
1171
+ );
1172
+ const taskLabel = TASK_TYPE_LABELS[request.task_type] ?? TASK_TYPE_LABELS["default"];
1173
+ const taskColor = TASK_TYPE_STYLES[request.task_type] ?? TASK_TYPE_STYLES["default"];
1174
+ return /* @__PURE__ */ jsxs9(
1175
+ Box8,
1176
+ {
1177
+ flexDirection: "column",
1178
+ borderStyle: "round",
1179
+ borderColor: THEME_COLORS.warning,
1180
+ paddingX: 2,
1181
+ paddingY: 1,
1182
+ marginTop: 1,
1183
+ children: [
1184
+ /* @__PURE__ */ jsxs9(Box8, { flexDirection: "row", children: [
1185
+ /* @__PURE__ */ jsx8(Text9, { bold: true, color: THEME_COLORS.warning, children: "\u26A0 \uC2B9\uC778 \uC694\uCCAD " }),
1186
+ /* @__PURE__ */ jsxs9(Text9, { color: taskColor, children: [
1187
+ "[",
1188
+ taskLabel,
1189
+ "]"
1190
+ ] })
1191
+ ] }),
1192
+ safeDescription && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text9, { wrap: "wrap", children: safeDescription }) }),
1193
+ request.planned_tools && request.planned_tools.length > 0 && /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", marginTop: 1, children: [
1194
+ /* @__PURE__ */ jsx8(Text9, { color: THEME_COLORS.muted, children: "\uC2E4\uD589 \uC608\uC815 \uB3C4\uAD6C:" }),
1195
+ request.planned_tools.map((tool, i) => /* @__PURE__ */ jsx8(Text9, { color: THEME_COLORS.muted, children: ` \u2022 ${tool}` }, i))
1196
+ ] }),
1197
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { bold: true, children: [
1198
+ "\uC2B9\uC778\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? ",
1199
+ /* @__PURE__ */ jsx8(Text9, { color: "green", children: "y" }),
1200
+ " / ",
1201
+ /* @__PURE__ */ jsx8(Text9, { color: "red", children: "n" })
1202
+ ] }) })
1203
+ ]
1204
+ }
1205
+ );
1206
+ }
1207
+
1208
+ // src/App.tsx
1209
+ import { jsx as jsx9, jsxs as jsxs10 } from "react/jsx-runtime";
1210
+ var BANNER_ITEM = { _banner: true };
1211
+ var initialState = {
1212
+ messages: [],
1213
+ isLoading: false,
1214
+ streamingContent: "",
1215
+ streamingThinking: "",
1216
+ pendingThinking: [],
1217
+ activeTools: [],
1218
+ sessionId: null,
1219
+ threadId: null,
1220
+ pendingApproval: null,
1221
+ statusLabel: null,
1222
+ error: null,
1223
+ theme: "dark",
1224
+ apiBase: getBaseUrl(),
1225
+ apiVersion: "v3"
1226
+ };
1227
+ function reducer(state, action) {
1228
+ switch (action.type) {
1229
+ case "SET_LOADING":
1230
+ return { ...state, isLoading: action.payload };
1231
+ case "ADD_USER_MESSAGE": {
1232
+ const msg = {
1233
+ ...action.payload,
1234
+ role: "user",
1235
+ content: action.payload.content
1236
+ };
1237
+ return { ...state, messages: [...state.messages, msg] };
1238
+ }
1239
+ case "START_ASSISTANT_MESSAGE": {
1240
+ const msg = {
1241
+ id: action.payload.id,
1242
+ role: "assistant",
1243
+ content: "",
1244
+ timestamp: action.payload.timestamp,
1245
+ streaming: true
1246
+ };
1247
+ return {
1248
+ ...state,
1249
+ messages: [...state.messages, msg],
1250
+ streamingContent: "",
1251
+ streamingThinking: "",
1252
+ pendingThinking: [],
1253
+ activeTools: []
1254
+ };
1255
+ }
1256
+ case "APPEND_STREAMING_CONTENT":
1257
+ return {
1258
+ ...state,
1259
+ streamingContent: state.streamingContent + action.payload
1260
+ };
1261
+ case "APPEND_STREAMING_THINKING":
1262
+ return {
1263
+ ...state,
1264
+ streamingThinking: state.streamingThinking + action.payload
1265
+ };
1266
+ case "START_THINKING_STEP": {
1267
+ const { iteration } = action.payload;
1268
+ const already = state.pendingThinking.some((s) => s.iteration === iteration);
1269
+ if (already) return state;
1270
+ return {
1271
+ ...state,
1272
+ pendingThinking: [
1273
+ ...state.pendingThinking,
1274
+ { iteration, content: "" }
1275
+ ],
1276
+ streamingThinking: ""
1277
+ };
1278
+ }
1279
+ case "END_THINKING_STEP": {
1280
+ const { iteration, tool_calls } = action.payload;
1281
+ const updated = state.pendingThinking.map(
1282
+ (step) => step.iteration === iteration ? { ...step, content: step.content + state.streamingThinking, tool_calls } : step
1283
+ );
1284
+ return {
1285
+ ...state,
1286
+ pendingThinking: updated,
1287
+ streamingThinking: ""
1288
+ };
1289
+ }
1290
+ case "MARK_TOOL_START":
1291
+ return {
1292
+ ...state,
1293
+ activeTools: [
1294
+ ...state.activeTools,
1295
+ { tool: action.payload.tool, pending: true }
1296
+ ]
1297
+ };
1298
+ case "MARK_TOOL_END": {
1299
+ const { tool, success } = action.payload;
1300
+ const idx = state.activeTools.findIndex((t) => t.tool === tool && t.pending);
1301
+ if (idx === -1) return state;
1302
+ const updated = [...state.activeTools];
1303
+ updated[idx] = { ...updated[idx], pending: false, success };
1304
+ return { ...state, activeTools: updated };
1305
+ }
1306
+ case "FINALIZE_ASSISTANT_MESSAGE": {
1307
+ const {
1308
+ messageId,
1309
+ content,
1310
+ evidence,
1311
+ metadata,
1312
+ sessionId,
1313
+ threadId
1314
+ } = action.payload;
1315
+ const messages = state.messages.map(
1316
+ (msg) => msg.id === messageId ? {
1317
+ ...msg,
1318
+ content,
1319
+ streaming: false,
1320
+ thinking: state.pendingThinking,
1321
+ tools: state.activeTools,
1322
+ evidence,
1323
+ metadata
1324
+ } : msg
1325
+ );
1326
+ return {
1327
+ ...state,
1328
+ messages,
1329
+ streamingContent: "",
1330
+ streamingThinking: "",
1331
+ pendingThinking: [],
1332
+ activeTools: [],
1333
+ sessionId,
1334
+ threadId,
1335
+ isLoading: false,
1336
+ statusLabel: null,
1337
+ error: null
1338
+ };
1339
+ }
1340
+ case "SET_PENDING_APPROVAL":
1341
+ return { ...state, pendingApproval: action.payload };
1342
+ case "SET_STATUS_LABEL":
1343
+ return { ...state, statusLabel: action.payload };
1344
+ case "SET_ERROR":
1345
+ return { ...state, error: action.payload, isLoading: false };
1346
+ case "SET_THEME":
1347
+ return { ...state, theme: action.payload };
1348
+ case "SET_API_BASE":
1349
+ return { ...state, apiBase: action.payload };
1350
+ case "SET_API_VERSION":
1351
+ return { ...state, apiVersion: action.payload };
1352
+ case "RESET_SESSION":
1353
+ return { ...initialState, apiBase: state.apiBase, theme: state.theme };
1354
+ default:
1355
+ return state;
1356
+ }
1357
+ }
1358
+ function App({ version, initialQuery }) {
1359
+ const { exit } = useApp();
1360
+ const { stdout } = useStdout();
1361
+ const [state, dispatch] = useReducer2(reducer, initialState);
1362
+ const client = useMemo2(() => new GovOnClient(state.apiBase), [state.apiBase]);
1363
+ const daemon = useDaemon(client);
1364
+ const { submit, cancel } = useSSE({
1365
+ client,
1366
+ dispatch,
1367
+ sessionId: state.sessionId
1368
+ });
1369
+ const { push: pushHistory } = useHistory();
1370
+ const handleSubmit = useCallback5(
1371
+ (query2) => {
1372
+ if (state.isLoading) return;
1373
+ const userMsgId = crypto.randomUUID();
1374
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1375
+ dispatch({
1376
+ type: "ADD_USER_MESSAGE",
1377
+ payload: { id: userMsgId, content: query2, timestamp }
1378
+ });
1379
+ pushHistory(query2);
1380
+ submit(query2);
1381
+ },
1382
+ [state.isLoading, dispatch, pushHistory, submit]
1383
+ );
1384
+ const handleApproval = useCallback5(
1385
+ async (approved) => {
1386
+ if (!state.threadId || !state.pendingApproval) return;
1387
+ dispatch({ type: "SET_PENDING_APPROVAL", payload: null });
1388
+ try {
1389
+ const result = await client.approve(state.threadId, approved);
1390
+ if (approved && result.text) {
1391
+ const streamingMsg = state.messages.find((m) => m.streaming);
1392
+ if (streamingMsg) {
1393
+ dispatch({
1394
+ type: "FINALIZE_ASSISTANT_MESSAGE",
1395
+ payload: {
1396
+ messageId: streamingMsg.id,
1397
+ content: result.text,
1398
+ evidence: result.evidence_items ?? [],
1399
+ metadata: {},
1400
+ sessionId: result.session_id,
1401
+ threadId: result.thread_id
1402
+ }
1403
+ });
1404
+ }
1405
+ }
1406
+ dispatch({ type: "SET_LOADING", payload: false });
1407
+ } catch (err) {
1408
+ const msg = err instanceof Error ? err.message : String(err);
1409
+ dispatch({ type: "SET_ERROR", payload: msg });
1410
+ dispatch({ type: "SET_LOADING", payload: false });
1411
+ }
1412
+ },
1413
+ [client, state.threadId, state.pendingApproval, state.messages, dispatch]
1414
+ );
1415
+ const initialSubmittedRef = React5.useRef(false);
1416
+ React5.useEffect(() => {
1417
+ if (daemon.ready && initialQuery && !initialSubmittedRef.current) {
1418
+ initialSubmittedRef.current = true;
1419
+ handleSubmit(initialQuery);
1420
+ }
1421
+ }, [daemon.ready, initialQuery, handleSubmit]);
1422
+ useInput2((input, key) => {
1423
+ if (key.ctrl && input === "d") exit();
1424
+ if (key.ctrl && input === "c") exit();
1425
+ if (key.escape && state.isLoading) cancel();
1426
+ });
1427
+ if (daemon.waiting) {
1428
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
1429
+ /* @__PURE__ */ jsx9(Banner, { version }),
1430
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text10, { color: THEME_COLORS.muted, children: "\uC11C\uBC84 \uC5F0\uACB0 \uC911\u2026" }) })
1431
+ ] });
1432
+ }
1433
+ if (daemon.error) {
1434
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
1435
+ /* @__PURE__ */ jsx9(Banner, { version }),
1436
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text10, { color: THEME_COLORS.error, children: [
1437
+ "\uC11C\uBC84 \uC624\uB958: ",
1438
+ daemon.error
1439
+ ] }) })
1440
+ ] });
1441
+ }
1442
+ const completedMessages = state.messages.filter((m) => !m.streaming);
1443
+ const staticItems = useMemo2(
1444
+ () => [BANNER_ITEM, ...completedMessages],
1445
+ [completedMessages]
1446
+ );
1447
+ const cols = stdout?.columns ?? 80;
1448
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
1449
+ /* @__PURE__ */ jsx9(Static, { items: staticItems, children: (item) => {
1450
+ if ("_banner" in item && item._banner) {
1451
+ return /* @__PURE__ */ jsx9(Banner, { version }, "__banner__");
1452
+ }
1453
+ const msg = item;
1454
+ return /* @__PURE__ */ jsx9(MessageBubble, { message: msg }, msg.id);
1455
+ } }),
1456
+ state.isLoading && /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", marginTop: 1, children: [
1457
+ state.streamingThinking.length > 0 && /* @__PURE__ */ jsx9(ThinkingBlock, { content: state.streamingThinking, streaming: true }),
1458
+ state.activeTools.length > 0 && /* @__PURE__ */ jsx9(ToolPanel, { tools: state.activeTools }),
1459
+ state.streamingContent.length > 0 ? /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(MarkdownView, { content: state.streamingContent, streaming: true }) }) : /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Spinner, {}) }),
1460
+ state.statusLabel && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text10, { color: THEME_COLORS.muted, dimColor: true, children: state.statusLabel }) })
1461
+ ] }),
1462
+ state.error && !state.isLoading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, marginLeft: 2, children: /* @__PURE__ */ jsxs10(Text10, { color: THEME_COLORS.error, children: [
1463
+ "\uC624\uB958: ",
1464
+ state.error
1465
+ ] }) }),
1466
+ state.pendingApproval && /* @__PURE__ */ jsx9(
1467
+ ApprovalPrompt,
1468
+ {
1469
+ request: state.pendingApproval,
1470
+ onApprove: handleApproval
1471
+ }
1472
+ ),
1473
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text10, { color: THEME_COLORS.muted, dimColor: true, children: "\u2500".repeat(Math.max(1, cols - 1)) }) }),
1474
+ !state.pendingApproval && /* @__PURE__ */ jsx9(InputBar, { onSubmit: handleSubmit, disabled: state.isLoading }),
1475
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text10, { dimColor: true, color: THEME_COLORS.dimmed, children: "esc \uCDE8\uC18C \xB7 Ctrl+D \uC885\uB8CC \xB7 /help \uB3C4\uC6C0\uB9D0" }) })
1476
+ ] });
1477
+ }
1478
+
1479
+ // src/index.tsx
1480
+ import { jsx as jsx10 } from "react/jsx-runtime";
1481
+ var require2 = createRequire(import.meta.url);
1482
+ var pkg = require2("../package.json");
1483
+ var argv = yargs(hideBin(process.argv)).scriptName("govon").usage("$0 [query]", "GovOn agentic TUI for civil complaint assistance").positional("query", {
1484
+ type: "string",
1485
+ describe: "Initial query to submit immediately"
1486
+ }).option("version", {
1487
+ alias: "v",
1488
+ type: "boolean",
1489
+ describe: "Show version"
1490
+ }).help().parseSync();
1491
+ if (argv.version) {
1492
+ process.stdout.write(pkg.version + "\n");
1493
+ process.exit(0);
1494
+ }
1495
+ var query = argv._[0];
1496
+ render(/* @__PURE__ */ jsx10(App, { version: pkg.version, initialQuery: query }));