xgen-dex-cli 1.4.1 → 1.6.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.
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  DexError,
3
+ dataDirectory,
3
4
  publicError
4
5
  } from "./chunk-ULMEXJOR.js";
5
6
 
@@ -8,11 +9,11 @@ import { render } from "ink";
8
9
  import { stdout } from "node:process";
9
10
 
10
11
  // src/tui/app.tsx
11
- import { useCallback, useEffect as useEffect7, useState as useState10 } from "react";
12
+ import { useCallback, useEffect as useEffect8, useState as useState11 } from "react";
12
13
  import { Box as Box10, Text as Text10, useApp as useApp2, useInput as useInput9 } from "ink";
13
14
 
14
15
  // src/tui/dashboard.tsx
15
- import { useEffect as useEffect5, useReducer, useRef as useRef2, useState as useState6 } from "react";
16
+ import { useEffect as useEffect6, useReducer, useRef as useRef3, useState as useState7 } from "react";
16
17
  import { Box as Box6, Text as Text6, useApp, useInput as useInput5 } from "ink";
17
18
 
18
19
  // src/tui/chat-state.ts
@@ -134,17 +135,161 @@ function chatReducer(state, action) {
134
135
  }
135
136
  }
136
137
 
138
+ // src/tui/measure.ts
139
+ import { useEffect, useRef, useState } from "react";
140
+ function placementOf(node) {
141
+ const yogaSelf = node?.yogaNode;
142
+ if (!node || !yogaSelf) return void 0;
143
+ let x = 0;
144
+ let y = 0;
145
+ for (let current = node; current; current = current.parentNode) {
146
+ const yoga = current.yogaNode;
147
+ if (!yoga) continue;
148
+ x += yoga.getComputedLeft();
149
+ y += yoga.getComputedTop();
150
+ }
151
+ return { x, y, width: yogaSelf.getComputedWidth(), height: yogaSelf.getComputedHeight() };
152
+ }
153
+ function useMeasured() {
154
+ const ref = useRef(null);
155
+ const [placement, setPlacement] = useState(void 0);
156
+ useEffect(() => {
157
+ const measured = placementOf(ref.current);
158
+ if (!measured) return;
159
+ if (placement?.x !== measured.x || placement?.y !== measured.y || placement?.width !== measured.width || placement?.height !== measured.height) {
160
+ setPlacement(measured);
161
+ }
162
+ });
163
+ return [ref, placement];
164
+ }
165
+
166
+ // src/tui/transcript.ts
167
+ import stringWidth from "string-width";
168
+ var INDENT = " ";
169
+ function colorOf(role) {
170
+ if (role === "user") return "cyan";
171
+ if (role === "assistant") return "green";
172
+ if (role === "activity") return "yellow";
173
+ if (role === "system") return "red";
174
+ return void 0;
175
+ }
176
+ function labelOf(role, agentName) {
177
+ if (role === "user") return "You";
178
+ if (role === "assistant") return agentName;
179
+ if (role === "activity") return "Tool";
180
+ return "System";
181
+ }
182
+ function wrapToWidth(text, width) {
183
+ if (width <= 0) return [text];
184
+ const lines = [];
185
+ for (const paragraph of text.split("\n")) {
186
+ if (paragraph === "") {
187
+ lines.push("");
188
+ continue;
189
+ }
190
+ let line = "";
191
+ let lineWidth = 0;
192
+ const flush2 = () => {
193
+ lines.push(line);
194
+ line = "";
195
+ lineWidth = 0;
196
+ };
197
+ for (const word of paragraph.match(/\s+|\S+/g) ?? []) {
198
+ const wordWidth = stringWidth(word);
199
+ if (lineWidth > 0 && lineWidth + wordWidth > width) {
200
+ flush2();
201
+ if (/^\s+$/.test(word)) continue;
202
+ }
203
+ if (wordWidth <= width) {
204
+ line += word;
205
+ lineWidth += wordWidth;
206
+ continue;
207
+ }
208
+ for (const char of word) {
209
+ const charWidth = stringWidth(char);
210
+ if (lineWidth + charWidth > width) flush2();
211
+ line += char;
212
+ lineWidth += charWidth;
213
+ }
214
+ }
215
+ flush2();
216
+ }
217
+ return lines;
218
+ }
219
+ function ruleFor(label, width) {
220
+ const room = Math.max(0, width - stringWidth("\u2500\u2500 "));
221
+ let trimmed = "";
222
+ let used = 0;
223
+ for (const char of label) {
224
+ const charWidth = stringWidth(char);
225
+ if (used + charWidth > room) {
226
+ trimmed = trimmed.slice(0, -1) + "\u2026";
227
+ break;
228
+ }
229
+ trimmed += char;
230
+ used += charWidth;
231
+ }
232
+ const head = `\u2500\u2500 ${trimmed} `;
233
+ return head + "\u2500".repeat(Math.max(0, width - stringWidth(head)));
234
+ }
235
+ function renderTranscript(messages, agentName, width) {
236
+ const lines = [];
237
+ const bodyWidth = Math.max(1, width - INDENT.length);
238
+ for (const message of messages) {
239
+ const color = colorOf(message.role);
240
+ if (message.role === "activity") {
241
+ for (const [index, text] of wrapToWidth(message.text, bodyWidth).entries()) {
242
+ lines.push({
243
+ key: `${message.id}:${index}`,
244
+ text: `${index === 0 ? "\xB7 " : INDENT}${text}`,
245
+ role: "activity",
246
+ color
247
+ });
248
+ }
249
+ continue;
250
+ }
251
+ if (lines.length > 0) lines.push({ key: `${message.id}:gap`, text: "", role: "text" });
252
+ lines.push({
253
+ key: `${message.id}:label`,
254
+ text: ruleFor(labelOf(message.role, agentName), width),
255
+ role: "label",
256
+ color
257
+ });
258
+ const body = message.text || (message.role === "assistant" ? "\u2026" : "");
259
+ for (const [index, text] of wrapToWidth(body, bodyWidth).entries()) {
260
+ lines.push({
261
+ key: `${message.id}:${index}`,
262
+ text: INDENT + text,
263
+ role: message.role === "system" ? "system" : "text",
264
+ color: message.role === "system" ? color : void 0
265
+ });
266
+ }
267
+ }
268
+ return lines;
269
+ }
270
+ function viewportOf(lines, height, scrollUp) {
271
+ if (height <= 0) return { lines: [], above: lines.length, below: 0 };
272
+ const maximumScroll2 = Math.max(0, lines.length - height);
273
+ const up = Math.min(Math.max(0, scrollUp), maximumScroll2);
274
+ const end = lines.length - up;
275
+ const start = Math.max(0, end - height);
276
+ return { lines: lines.slice(start, end), above: start, below: lines.length - end };
277
+ }
278
+ function maximumScroll(lineCount, height) {
279
+ return Math.max(0, lineCount - Math.max(0, height));
280
+ }
281
+
137
282
  // src/tui/command-palette.tsx
138
- import { useState as useState2 } from "react";
283
+ import { useState as useState3 } from "react";
139
284
  import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
140
285
 
141
286
  // src/tui/components.tsx
142
287
  import { Box as Box2, Text as Text2 } from "ink";
143
288
 
144
289
  // src/tui/ime-text-input.tsx
145
- import { useEffect, useRef, useState } from "react";
290
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
146
291
  import { Box, Text, useCursor, useInput } from "ink";
147
- import stringWidth from "string-width";
292
+ import stringWidth2 from "string-width";
148
293
 
149
294
  // src/tui/terminal-input.ts
150
295
  var PASTE_START = "[200~";
@@ -160,6 +305,155 @@ function classifyInput(input, pasting = false) {
160
305
  return text ? { kind: "text", text } : { kind: "ignore" };
161
306
  }
162
307
 
308
+ // src/tui/hangul.ts
309
+ var CHO = "\u3131\u3132\u3134\u3137\u3138\u3139\u3141\u3142\u3143\u3145\u3146\u3147\u3148\u3149\u314A\u314B\u314C\u314D\u314E";
310
+ var JUNG = "\u314F\u3150\u3151\u3152\u3153\u3154\u3155\u3156\u3157\u3158\u3159\u315A\u315B\u315C\u315D\u315E\u315F\u3160\u3161\u3162\u3163";
311
+ var JONG = " \u3131\u3132\u3133\u3134\u3135\u3136\u3137\u3139\u313A\u313B\u313C\u313D\u313E\u313F\u3140\u3141\u3142\u3144\u3145\u3146\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
312
+ var SYLLABLE_BASE = 44032;
313
+ var KEYS = {
314
+ r: "\u3131",
315
+ R: "\u3132",
316
+ s: "\u3134",
317
+ e: "\u3137",
318
+ E: "\u3138",
319
+ f: "\u3139",
320
+ a: "\u3141",
321
+ q: "\u3142",
322
+ Q: "\u3143",
323
+ t: "\u3145",
324
+ T: "\u3146",
325
+ d: "\u3147",
326
+ w: "\u3148",
327
+ W: "\u3149",
328
+ c: "\u314A",
329
+ z: "\u314B",
330
+ x: "\u314C",
331
+ v: "\u314D",
332
+ g: "\u314E",
333
+ k: "\u314F",
334
+ o: "\u3150",
335
+ i: "\u3151",
336
+ O: "\u3152",
337
+ j: "\u3153",
338
+ p: "\u3154",
339
+ u: "\u3155",
340
+ P: "\u3156",
341
+ h: "\u3157",
342
+ y: "\u315B",
343
+ n: "\u315C",
344
+ b: "\u3160",
345
+ m: "\u3161",
346
+ l: "\u3163"
347
+ };
348
+ var VOWEL_PAIRS = {
349
+ "\u3157\u314F": "\u3158",
350
+ "\u3157\u3150": "\u3159",
351
+ "\u3157\u3163": "\u315A",
352
+ "\u315C\u3153": "\u315D",
353
+ "\u315C\u3154": "\u315E",
354
+ "\u315C\u3163": "\u315F",
355
+ "\u3161\u3163": "\u3162"
356
+ };
357
+ var FINAL_PAIRS = {
358
+ "\u3131\u3145": "\u3133",
359
+ "\u3134\u3148": "\u3135",
360
+ "\u3134\u314E": "\u3136",
361
+ "\u3139\u3131": "\u313A",
362
+ "\u3139\u3141": "\u313B",
363
+ "\u3139\u3142": "\u313C",
364
+ "\u3139\u3145": "\u313D",
365
+ "\u3139\u314C": "\u313E",
366
+ "\u3139\u314D": "\u313F",
367
+ "\u3139\u314E": "\u3140",
368
+ "\u3142\u3145": "\u3144"
369
+ };
370
+ var SPLIT = Object.fromEntries(
371
+ [...Object.entries(VOWEL_PAIRS), ...Object.entries(FINAL_PAIRS)].map(([pair, joined]) => [
372
+ joined,
373
+ [pair[0], pair[1]]
374
+ ])
375
+ );
376
+ var EMPTY = {};
377
+ function jamoOf(key) {
378
+ if (KEYS[key]) return KEYS[key];
379
+ const lower = key.toLowerCase();
380
+ return lower === key ? void 0 : KEYS[lower];
381
+ }
382
+ function isVowel(jamo) {
383
+ return JUNG.includes(jamo) || jamo === "\u3157" || jamo === "\u315C" || jamo === "\u3161";
384
+ }
385
+ function canBeFinal(jamo) {
386
+ return JONG.indexOf(jamo) > 0;
387
+ }
388
+ function display(state) {
389
+ const { cho, jung, jong } = state;
390
+ if (cho && jung) {
391
+ const l = CHO.indexOf(cho);
392
+ const v = JUNG.indexOf(jung);
393
+ const t = jong ? JONG.indexOf(jong) : 0;
394
+ if (l >= 0 && v >= 0 && t >= 0) {
395
+ return String.fromCharCode(SYLLABLE_BASE + (l * 21 + v) * 28 + t);
396
+ }
397
+ }
398
+ return cho ?? jung ?? "";
399
+ }
400
+ function feed(state, jamo) {
401
+ const { cho, jung, jong } = state;
402
+ if (isVowel(jamo)) {
403
+ if (cho && jung && jong) {
404
+ const parts = SPLIT[jong];
405
+ const moved = parts ? parts[1] : jong;
406
+ const kept = parts ? parts[0] : void 0;
407
+ return {
408
+ commit: display({ cho, jung, jong: kept }),
409
+ state: { cho: moved, jung: jamo }
410
+ };
411
+ }
412
+ if (cho && jung) {
413
+ const merged = VOWEL_PAIRS[jung + jamo];
414
+ if (merged) return { commit: "", state: { cho, jung: merged } };
415
+ return { commit: display(state), state: { jung: jamo } };
416
+ }
417
+ if (cho) return { commit: "", state: { cho, jung: jamo } };
418
+ if (jung) {
419
+ const merged = VOWEL_PAIRS[jung + jamo];
420
+ if (merged) return { commit: "", state: { jung: merged } };
421
+ return { commit: jung, state: { jung: jamo } };
422
+ }
423
+ return { commit: "", state: { jung: jamo } };
424
+ }
425
+ if (cho && jung) {
426
+ if (!jong) {
427
+ if (canBeFinal(jamo)) return { commit: "", state: { cho, jung, jong: jamo } };
428
+ return { commit: display(state), state: { cho: jamo } };
429
+ }
430
+ const merged = FINAL_PAIRS[jong + jamo];
431
+ if (merged) return { commit: "", state: { cho, jung, jong: merged } };
432
+ return { commit: display(state), state: { cho: jamo } };
433
+ }
434
+ if (cho) {
435
+ return { commit: cho, state: { cho: jamo } };
436
+ }
437
+ if (jung) return { commit: jung, state: { cho: jamo } };
438
+ return { commit: "", state: { cho: jamo } };
439
+ }
440
+ function back(state) {
441
+ const { cho, jung, jong } = state;
442
+ if (jong) {
443
+ const parts = SPLIT[jong];
444
+ return { state: { cho, jung, jong: parts ? parts[0] : void 0 }, handled: true };
445
+ }
446
+ if (jung) {
447
+ const parts = SPLIT[jung];
448
+ return { state: { cho, jung: parts ? parts[0] : void 0 }, handled: true };
449
+ }
450
+ if (cho) return { state: EMPTY, handled: true };
451
+ return { state: EMPTY, handled: false };
452
+ }
453
+ function flush(state) {
454
+ return { commit: display(state), state: EMPTY };
455
+ }
456
+
163
457
  // src/tui/ime-text-input.tsx
164
458
  import { jsx, jsxs } from "react/jsx-runtime";
165
459
  var segmenter = new Intl.Segmenter("ko", { granularity: "grapheme" });
@@ -169,26 +463,13 @@ function graphemes(value) {
169
463
  function clamp(value, minimum, maximum) {
170
464
  return Math.min(Math.max(value, minimum), maximum);
171
465
  }
172
- function placementOf(node) {
173
- const width = node?.yogaNode?.getComputedWidth();
174
- if (!node || width === void 0) return void 0;
175
- let x = 0;
176
- let y = 0;
177
- for (let current = node; current; current = current.parentNode) {
178
- const yoga = current.yogaNode;
179
- if (!yoga) continue;
180
- x += yoga.getComputedLeft();
181
- y += yoga.getComputedTop();
182
- }
183
- return { x, y, width };
184
- }
185
466
  function visibleInput(segments, cursor, maximumWidth) {
186
467
  let start = cursor;
187
468
  let widthBeforeCursor = 0;
188
- const followingWidth = cursor < segments.length ? stringWidth(segments[cursor] ?? "") : 0;
469
+ const followingWidth = cursor < segments.length ? stringWidth2(segments[cursor] ?? "") : 0;
189
470
  const beforeLimit = Math.max(0, maximumWidth - Math.min(followingWidth, maximumWidth));
190
471
  while (start > 0) {
191
- const width = stringWidth(segments[start - 1] ?? "");
472
+ const width = stringWidth2(segments[start - 1] ?? "");
192
473
  if (widthBeforeCursor + width > beforeLimit) break;
193
474
  widthBeforeCursor += width;
194
475
  start -= 1;
@@ -196,7 +477,7 @@ function visibleInput(segments, cursor, maximumWidth) {
196
477
  let end = cursor;
197
478
  let totalWidth = widthBeforeCursor;
198
479
  while (end < segments.length) {
199
- const width = stringWidth(segments[end] ?? "");
480
+ const width = stringWidth2(segments[end] ?? "");
200
481
  if (totalWidth + width > maximumWidth) break;
201
482
  totalWidth += width;
202
483
  end += 1;
@@ -212,20 +493,13 @@ function TerminalCursor({ x, y }) {
212
493
  return null;
213
494
  }
214
495
  function ImeTextInput(props) {
215
- const ref = useRef(null);
216
- const [placement, setPlacement] = useState(void 0);
496
+ const [ref, placement] = useMeasured();
217
497
  const initialSegments = graphemes(props.value);
218
- const [cursor, setCursor] = useState(initialSegments.length);
219
- const valueRef = useRef(props.value);
220
- const cursorRef = useRef(initialSegments.length);
221
- const pastingRef = useRef(false);
222
- useEffect(() => {
223
- const measured = placementOf(ref.current);
224
- if (!measured) return;
225
- if (placement?.x !== measured.x || placement?.y !== measured.y || placement?.width !== measured.width) {
226
- setPlacement(measured);
227
- }
228
- });
498
+ const [cursor, setCursor] = useState2(initialSegments.length);
499
+ const valueRef = useRef2(props.value);
500
+ const cursorRef = useRef2(initialSegments.length);
501
+ const pastingRef = useRef2(false);
502
+ const composingRef = useRef2(EMPTY);
229
503
  const moveCursor = (next, length) => {
230
504
  const resolved = clamp(next, 0, length);
231
505
  cursorRef.current = resolved;
@@ -237,7 +511,10 @@ function ImeTextInput(props) {
237
511
  moveCursor(nextCursor, segments.length);
238
512
  props.onChange(nextValue);
239
513
  };
240
- useEffect(() => {
514
+ useEffect2(() => {
515
+ if (!props.focus || !props.hangulMode) composingRef.current = EMPTY;
516
+ }, [props.focus, props.hangulMode]);
517
+ useEffect2(() => {
241
518
  if (props.value === valueRef.current) return;
242
519
  const previousLength = graphemes(valueRef.current).length;
243
520
  const nextLength = graphemes(props.value).length;
@@ -245,12 +522,24 @@ function ImeTextInput(props) {
245
522
  valueRef.current = props.value;
246
523
  moveCursor(wasAtEnd ? nextLength : cursorRef.current, nextLength);
247
524
  }, [props.value]);
525
+ const applyComposition = (segments, cursor2, previous, commit, next) => {
526
+ const removed = previous ? 1 : 0;
527
+ const inserted = graphemes(commit + next);
528
+ segments.splice(cursor2 - removed, removed, ...inserted);
529
+ updateValue(segments, cursor2 - removed + inserted.length);
530
+ };
248
531
  useInput(
249
532
  (input, key) => {
250
533
  const current = graphemes(valueRef.current);
251
534
  const currentCursor = clamp(cursorRef.current, 0, current.length);
252
535
  const event = classifyInput(input, pastingRef.current);
536
+ const composing = composingRef.current;
537
+ const shown = display(composing);
538
+ const settle = () => {
539
+ composingRef.current = EMPTY;
540
+ };
253
541
  if (event.kind === "paste-start") {
542
+ settle();
254
543
  pastingRef.current = true;
255
544
  return;
256
545
  }
@@ -266,36 +555,81 @@ function ImeTextInput(props) {
266
555
  }
267
556
  return;
268
557
  }
558
+ if (key.ctrl && (input === "`" || input === "l")) {
559
+ settle();
560
+ props.onHangulModeChange?.(!props.hangulMode);
561
+ return;
562
+ }
269
563
  if (key.return) {
564
+ settle();
270
565
  props.onSubmit?.(valueRef.current);
271
566
  return;
272
567
  }
273
568
  if (key.leftArrow) {
569
+ settle();
274
570
  moveCursor(currentCursor - 1, current.length);
275
571
  return;
276
572
  }
277
573
  if (key.rightArrow) {
574
+ settle();
278
575
  moveCursor(currentCursor + 1, current.length);
279
576
  return;
280
577
  }
281
578
  if (key.home) {
579
+ settle();
282
580
  moveCursor(0, current.length);
283
581
  return;
284
582
  }
285
583
  if (key.end) {
584
+ settle();
286
585
  moveCursor(current.length, current.length);
287
586
  return;
288
587
  }
289
588
  if (key.backspace || key.delete) {
589
+ const stepped = back(composing);
590
+ if (stepped.handled) {
591
+ composingRef.current = stepped.state;
592
+ applyComposition(current, currentCursor, shown, "", display(stepped.state));
593
+ return;
594
+ }
290
595
  if (currentCursor === 0) return;
291
596
  current.splice(currentCursor - 1, 1);
292
597
  updateValue(current, currentCursor - 1);
293
598
  return;
294
599
  }
295
600
  if (key.ctrl || key.meta || key.tab || key.escape || key.upArrow || key.downArrow || key.pageUp || key.pageDown) {
601
+ settle();
296
602
  return;
297
603
  }
298
604
  if (event.kind !== "text") return;
605
+ if (props.hangulMode) {
606
+ let state = composing;
607
+ let previous = shown;
608
+ let segments = current;
609
+ let cursor2 = currentCursor;
610
+ for (const character of event.text) {
611
+ const jamo = jamoOf(character);
612
+ if (!jamo) {
613
+ const flushed = flush(state);
614
+ state = flushed.state;
615
+ applyComposition(segments, cursor2, previous, flushed.commit + character, "");
616
+ segments = graphemes(valueRef.current);
617
+ cursor2 = cursorRef.current;
618
+ previous = "";
619
+ continue;
620
+ }
621
+ const result = feed(state, jamo);
622
+ state = result.state;
623
+ const next = display(state);
624
+ applyComposition(segments, cursor2, previous, result.commit, next);
625
+ segments = graphemes(valueRef.current);
626
+ cursor2 = cursorRef.current;
627
+ previous = next;
628
+ }
629
+ composingRef.current = state;
630
+ return;
631
+ }
632
+ settle();
299
633
  const inserted = graphemes(event.text);
300
634
  current.splice(currentCursor, 0, ...inserted);
301
635
  updateValue(current, currentCursor + inserted.length);
@@ -368,7 +702,7 @@ function FormField(props) {
368
702
  // src/tui/command-palette.tsx
369
703
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
370
704
  function CommandPalette(props) {
371
- const [cursor, setCursor] = useState2(0);
705
+ const [cursor, setCursor] = useState3(0);
372
706
  useInput2((_input, key) => {
373
707
  if (key.escape) props.onCancel();
374
708
  if (key.upArrow) setCursor((current) => Math.max(0, current - 1));
@@ -387,15 +721,15 @@ function CommandPalette(props) {
387
721
  }
388
722
 
389
723
  // src/tui/history-screen.tsx
390
- import { useEffect as useEffect2, useState as useState3 } from "react";
724
+ import { useEffect as useEffect3, useState as useState4 } from "react";
391
725
  import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
392
726
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
393
727
  function HistoryScreen(props) {
394
- const [items, setItems] = useState3([]);
395
- const [cursor, setCursor] = useState3(0);
396
- const [loading, setLoading] = useState3(true);
397
- const [error, setError] = useState3();
398
- useEffect2(() => {
728
+ const [items, setItems] = useState4([]);
729
+ const [cursor, setCursor] = useState4(0);
730
+ const [loading, setLoading] = useState4(true);
731
+ const [error, setError] = useState4();
732
+ useEffect3(() => {
399
733
  let alive = true;
400
734
  props.engine.listConversations(props.profile).then((result) => alive && setItems(result)).catch((reason) => alive && setError(publicError(reason).message)).finally(() => alive && setLoading(false));
401
735
  return () => {
@@ -444,7 +778,7 @@ function HistoryScreen(props) {
444
778
  }
445
779
 
446
780
  // src/tui/start-panel.tsx
447
- import { useEffect as useEffect3, useState as useState4 } from "react";
781
+ import { useEffect as useEffect4, useState as useState5 } from "react";
448
782
  import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
449
783
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
450
784
  function StartPanel(props) {
@@ -452,10 +786,10 @@ function StartPanel(props) {
452
786
  { kind: "new" },
453
787
  ...props.conversations.map((conversation) => ({ kind: "conversation", conversation }))
454
788
  ];
455
- const [cursor, setCursor] = useState4(0);
456
- const [opening, setOpening] = useState4(false);
457
- const [error, setError] = useState4();
458
- useEffect3(() => {
789
+ const [cursor, setCursor] = useState5(0);
790
+ const [opening, setOpening] = useState5(false);
791
+ const [error, setError] = useState5();
792
+ useEffect4(() => {
459
793
  setCursor((current) => Math.min(current, Math.max(0, rows.length - 1)));
460
794
  }, [rows.length]);
461
795
  useInput4(
@@ -530,7 +864,7 @@ function when(conversation) {
530
864
  }
531
865
 
532
866
  // src/tui/use-terminal-size.ts
533
- import { useEffect as useEffect4, useState as useState5 } from "react";
867
+ import { useEffect as useEffect5, useState as useState6 } from "react";
534
868
  import { useStdout } from "ink";
535
869
  function useTerminalSize() {
536
870
  const { stdout: stdout2 } = useStdout();
@@ -539,8 +873,8 @@ function useTerminalSize() {
539
873
  const rows = stdout2.rows || 30;
540
874
  return { columns, rows, wide: columns >= 88 };
541
875
  };
542
- const [size, setSize] = useState5(read);
543
- useEffect4(() => {
876
+ const [size, setSize] = useState6(read);
877
+ useEffect5(() => {
544
878
  const resize = () => setSize(read());
545
879
  stdout2.on("resize", resize);
546
880
  return () => {
@@ -573,32 +907,39 @@ function AgentSidebar(props) {
573
907
  props.agents.length === 0 ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\uC0AC\uC6A9 \uAC00\uB2A5\uD55C Agent\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }) : null
574
908
  ] });
575
909
  }
576
- function messageColor(role) {
577
- if (role === "user") return "cyan";
578
- if (role === "assistant") return "green";
579
- if (role === "activity") return "yellow";
580
- if (role === "system") return "red";
581
- return void 0;
582
- }
583
- function labelOf(role, agentName) {
584
- if (role === "user") return "You";
585
- if (role === "assistant") return agentName;
586
- if (role === "activity") return "Tool";
587
- return "System";
588
- }
589
910
  function ChatPane(props) {
590
- const visibleCount = Math.max(4, Math.floor((props.height - 5) / 2));
591
- const visible = props.messages.slice(-visibleCount);
911
+ const [ref, box] = useMeasured();
912
+ const width = box?.width ?? 20;
913
+ const height = box?.height ?? 1;
914
+ const lines = renderTranscript(props.messages, props.agent?.workflowName ?? "Agent", width);
915
+ const view = viewportOf(lines, height, props.scrollUp);
916
+ useEffect6(() => {
917
+ props.onViewport(lines.length, height);
918
+ }, [lines.length, height]);
592
919
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "blue", paddingX: 1, children: [
593
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: props.agent?.workflowName ?? "Agent\uB97C \uC120\uD0DD\uD558\uC138\uC694" }),
594
- /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", flexGrow: 1, children: [
595
- visible.length === 0 ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: props.agent ? "\uBA54\uC2DC\uC9C0\uB97C \uC785\uB825\uD574 \uB300\uD654\uB97C \uC2DC\uC791\uD558\uC138\uC694." : "\uC67C\uCABD\uC5D0\uC11C Agent\uB97C \uC120\uD0DD\uD558\uC138\uC694." }) : null,
596
- visible.map((message) => /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: message.role === "activity" ? 0 : 1, children: [
597
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: messageColor(message.role), children: labelOf(message.role, props.agent?.workflowName ?? "Agent") }),
598
- /* @__PURE__ */ jsx6(Text6, { dimColor: message.role === "activity", children: message.text || (message.role === "assistant" ? "\u2026" : "") })
599
- ] }, message.id))
920
+ /* @__PURE__ */ jsxs6(Box6, { children: [
921
+ /* @__PURE__ */ jsx6(Text6, { bold: true, wrap: "truncate-end", children: props.agent?.workflowName ?? "Agent\uB97C \uC120\uD0DD\uD558\uC138\uC694" }),
922
+ view.below > 0 ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
923
+ " \xB7 \u2193",
924
+ view.below,
925
+ "\uC904"
926
+ ] }) : null
927
+ ] }),
928
+ /* @__PURE__ */ jsxs6(Box6, { ref, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [
929
+ view.lines.length === 0 ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: props.agent ? "\uBA54\uC2DC\uC9C0\uB97C \uC785\uB825\uD574 \uB300\uD654\uB97C \uC2DC\uC791\uD558\uC138\uC694." : "\uC67C\uCABD\uC5D0\uC11C Agent\uB97C \uC120\uD0DD\uD558\uC138\uC694." }) : null,
930
+ view.lines.map((line) => /* @__PURE__ */ jsx6(
931
+ Text6,
932
+ {
933
+ wrap: "truncate-end",
934
+ bold: line.role === "label",
935
+ dimColor: line.role === "activity",
936
+ color: line.color,
937
+ children: line.text || " "
938
+ },
939
+ line.key
940
+ ))
600
941
  ] }),
601
- props.status ? /* @__PURE__ */ jsxs6(Text6, { color: "yellow", children: [
942
+ props.status ? /* @__PURE__ */ jsxs6(Text6, { color: "yellow", wrap: "truncate-end", children: [
602
943
  "\u25C6 ",
603
944
  props.status
604
945
  ] }) : null
@@ -606,7 +947,8 @@ function ChatPane(props) {
606
947
  }
607
948
  function Composer(props) {
608
949
  return /* @__PURE__ */ jsxs6(Box6, { borderStyle: "round", borderColor: props.focused ? "cyan" : "gray", paddingX: 1, children: [
609
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "\u203A " }),
950
+ /* @__PURE__ */ jsx6(Text6, { color: props.hangulMode ? "yellow" : void 0, dimColor: !props.hangulMode, children: props.hangulMode ? "\uD55C" : "EN" }),
951
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: " \u203A " }),
610
952
  props.disabled ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\uC751\uB2F5\uC744 \uAE30\uB2E4\uB9AC\uB294 \uC911..." }) : /* @__PURE__ */ jsx6(
611
953
  ImeTextInput,
612
954
  {
@@ -614,7 +956,9 @@ function Composer(props) {
614
956
  onChange: props.onChange,
615
957
  onSubmit: props.onSubmit,
616
958
  focus: props.focused,
617
- placeholder: "\uBA54\uC2DC\uC9C0\uB97C \uC785\uB825\uD558\uC138\uC694"
959
+ placeholder: "\uBA54\uC2DC\uC9C0\uB97C \uC785\uB825\uD558\uC138\uC694",
960
+ hangulMode: props.hangulMode,
961
+ onHangulModeChange: props.onHangulModeChange
618
962
  }
619
963
  )
620
964
  ] });
@@ -623,20 +967,27 @@ function Dashboard(props) {
623
967
  const { exit } = useApp();
624
968
  const size = useTerminalSize();
625
969
  const bodyHeight = Math.max(12, size.rows - 5);
626
- const [focus, setFocus] = useState6("agents");
627
- const [cursor, setCursor] = useState6(0);
628
- const [selected, setSelected] = useState6(() => {
970
+ const [focus, setFocus] = useState7("agents");
971
+ const [cursor, setCursor] = useState7(0);
972
+ const [selected, setSelected] = useState7(() => {
629
973
  const first = props.session.agents[0];
630
974
  return first ? { workflowId: first.workflowId, workflowName: first.workflowName } : void 0;
631
975
  });
632
- const [input, setInput] = useState6("");
976
+ const [input, setInput] = useState7("");
633
977
  const [chat, dispatch] = useReducer(chatReducer, initialChatState);
634
- const [palette, setPalette] = useState6(false);
635
- const [history, setHistory] = useState6(false);
636
- const [start, setStart] = useState6();
637
- const [starting, setStarting] = useState6(false);
638
- const controller = useRef2(null);
639
- useEffect5(() => () => controller.current?.abort(), []);
978
+ const [palette, setPalette] = useState7(false);
979
+ const [history, setHistory] = useState7(false);
980
+ const [start, setStart] = useState7();
981
+ const [scrollUp, setScrollUp] = useState7(0);
982
+ const [hangulMode, setHangulMode] = useState7(props.preferences?.hangulMode ?? false);
983
+ const changeHangulMode = (enabled) => {
984
+ setHangulMode(enabled);
985
+ props.preferences?.onHangulModeChange?.(enabled);
986
+ };
987
+ const viewport = useRef3({ lineCount: 0, height: 0 });
988
+ const [starting, setStarting] = useState7(false);
989
+ const controller = useRef3(null);
990
+ useEffect6(() => () => controller.current?.abort(), []);
640
991
  const selectAgent = async () => {
641
992
  const agent = props.session.agents[cursor];
642
993
  if (!agent || chat.running || starting) return;
@@ -662,12 +1013,14 @@ function Dashboard(props) {
662
1013
  setSelected(ref);
663
1014
  dispatch({ type: "reset" });
664
1015
  setInput("");
1016
+ setScrollUp(0);
665
1017
  setStart(void 0);
666
1018
  setFocus("composer");
667
1019
  };
668
1020
  const openHistory = (conversation, turns) => {
669
1021
  setSelected({ workflowId: conversation.workflowId, workflowName: conversation.workflowName });
670
1022
  dispatch({ type: "history_loaded", interactionId: conversation.interactionId, turns });
1023
+ setScrollUp(0);
671
1024
  const index = props.session.agents.findIndex((agent) => agent.workflowId === conversation.workflowId);
672
1025
  if (index >= 0) setCursor(index);
673
1026
  setHistory(false);
@@ -682,6 +1035,7 @@ function Dashboard(props) {
682
1035
  if (chat.running) return;
683
1036
  dispatch({ type: "reset" });
684
1037
  setInput("");
1038
+ setScrollUp(0);
685
1039
  setPalette(false);
686
1040
  setFocus("composer");
687
1041
  };
@@ -697,6 +1051,7 @@ function Dashboard(props) {
697
1051
  input: text
698
1052
  });
699
1053
  setInput("");
1054
+ setScrollUp(0);
700
1055
  dispatch({ type: "turn_started", interactionId: resolved.interactionId, input: text });
701
1056
  const active = new AbortController();
702
1057
  controller.current = active;
@@ -712,6 +1067,12 @@ function Dashboard(props) {
712
1067
  controller.current = null;
713
1068
  }
714
1069
  };
1070
+ const scrollBy = (direction) => {
1071
+ const { lineCount, height } = viewport.current;
1072
+ const step = Math.max(1, Math.floor(height / 2));
1073
+ const limit = maximumScroll(lineCount, height);
1074
+ setScrollUp((current) => Math.min(limit, Math.max(0, current - direction * step)));
1075
+ };
715
1076
  useInput5(
716
1077
  (keyInput, key) => {
717
1078
  if (key.ctrl && keyInput === "k") setPalette(true);
@@ -722,6 +1083,8 @@ function Dashboard(props) {
722
1083
  if (chat.running) cancelTurn();
723
1084
  setHistory(true);
724
1085
  } else if (key.ctrl && keyInput === "n") newConversation();
1086
+ else if (key.pageUp) scrollBy(-1);
1087
+ else if (key.pageDown) scrollBy(1);
725
1088
  else if (key.escape && chat.running) cancelTurn();
726
1089
  else if (key.escape) setFocus("agents");
727
1090
  else if (key.tab) setFocus((current) => current === "agents" ? "composer" : "agents");
@@ -803,7 +1166,18 @@ function Dashboard(props) {
803
1166
  }
804
1167
  }
805
1168
  ) : /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", flexGrow: 1, children: [
806
- /* @__PURE__ */ jsx6(ChatPane, { agent: selected, messages: chat.messages, status: chat.status, height: bodyHeight - 3 }),
1169
+ /* @__PURE__ */ jsx6(
1170
+ ChatPane,
1171
+ {
1172
+ agent: selected,
1173
+ messages: chat.messages,
1174
+ status: chat.status,
1175
+ scrollUp,
1176
+ onViewport: (lineCount, height) => {
1177
+ viewport.current = { lineCount, height };
1178
+ }
1179
+ }
1180
+ ),
807
1181
  /* @__PURE__ */ jsx6(
808
1182
  Composer,
809
1183
  {
@@ -811,7 +1185,9 @@ function Dashboard(props) {
811
1185
  onChange: setInput,
812
1186
  onSubmit: (value) => void send(value),
813
1187
  focused: focus === "composer",
814
- disabled: chat.running || !selected
1188
+ disabled: chat.running || !selected,
1189
+ hangulMode,
1190
+ onHangulModeChange: changeHangulMode
815
1191
  }
816
1192
  )
817
1193
  ] });
@@ -830,18 +1206,18 @@ function Dashboard(props) {
830
1206
  }
831
1207
  ),
832
1208
  body,
833
- /* @__PURE__ */ jsx6(Footer, { text: "Tab \uD328\uB110 \xB7 Ctrl+K \uBA85\uB839 \xB7 Ctrl+H \uAE30\uB85D \xB7 Ctrl+P \uD504\uB85C\uD544 \xB7 Esc \uCDE8\uC18C \xB7 Ctrl+Q \uC885\uB8CC" })
1209
+ /* @__PURE__ */ jsx6(Footer, { text: "Ctrl+Space \uD55C/\uC601 \xB7 Tab \uD328\uB110 \xB7 PgUp/PgDn \uC2A4\uD06C\uB864 \xB7 Ctrl+K \uBA85\uB839 \xB7 Ctrl+H \uAE30\uB85D \xB7 Ctrl+P \uD504\uB85C\uD544 \xB7 Esc \uCDE8\uC18C \xB7 Ctrl+Q \uC885\uB8CC" })
834
1210
  ] });
835
1211
  }
836
1212
 
837
1213
  // src/tui/login-screen.tsx
838
- import { useState as useState7 } from "react";
1214
+ import { useState as useState8 } from "react";
839
1215
  import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
840
1216
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
841
1217
  function LoginScreen(props) {
842
- const [email, setEmail] = useState7("");
843
- const [password, setPassword] = useState7("");
844
- const [focus, setFocus] = useState7("email");
1218
+ const [email, setEmail] = useState8("");
1219
+ const [password, setPassword] = useState8("");
1220
+ const [focus, setFocus] = useState8("email");
845
1221
  useInput6(
846
1222
  (input, key) => {
847
1223
  if (key.tab) setFocus((current) => current === "email" ? "password" : "email");
@@ -902,16 +1278,16 @@ function LoginScreen(props) {
902
1278
  }
903
1279
 
904
1280
  // src/tui/profile-screen.tsx
905
- import { useEffect as useEffect6, useState as useState8 } from "react";
1281
+ import { useEffect as useEffect7, useState as useState9 } from "react";
906
1282
  import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
907
1283
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
908
1284
  function ProfileScreen(props) {
909
- const [cursor, setCursor] = useState8(Math.max(0, props.profiles.findIndex((profile) => profile.current)));
910
- const [creating, setCreating] = useState8(false);
911
- const [focus, setFocus] = useState8("name");
912
- const [name, setName] = useState8("");
913
- const [serverUrl, setServerUrl] = useState8("");
914
- useEffect6(() => setCursor((current) => Math.min(current, Math.max(0, props.profiles.length - 1))), [props.profiles]);
1285
+ const [cursor, setCursor] = useState9(Math.max(0, props.profiles.findIndex((profile) => profile.current)));
1286
+ const [creating, setCreating] = useState9(false);
1287
+ const [focus, setFocus] = useState9("name");
1288
+ const [name, setName] = useState9("");
1289
+ const [serverUrl, setServerUrl] = useState9("");
1290
+ useEffect7(() => setCursor((current) => Math.min(current, Math.max(0, props.profiles.length - 1))), [props.profiles]);
915
1291
  useInput7(
916
1292
  (input, key) => {
917
1293
  if (key.escape) {
@@ -995,11 +1371,11 @@ function ProfileScreen(props) {
995
1371
  }
996
1372
 
997
1373
  // src/tui/server-screen.tsx
998
- import { useState as useState9 } from "react";
1374
+ import { useState as useState10 } from "react";
999
1375
  import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
1000
1376
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1001
1377
  function ServerScreen(props) {
1002
- const [serverUrl, setServerUrl] = useState9(props.initialValue ?? "");
1378
+ const [serverUrl, setServerUrl] = useState10(props.initialValue ?? "");
1003
1379
  const editing = props.initialValue !== void 0;
1004
1380
  useInput8(
1005
1381
  (_input, key) => {
@@ -1038,15 +1414,18 @@ function ServerScreen(props) {
1038
1414
 
1039
1415
  // src/tui/app.tsx
1040
1416
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1041
- function App({ engine }) {
1417
+ function App({
1418
+ engine,
1419
+ preferences
1420
+ }) {
1042
1421
  const { exit } = useApp2();
1043
- const [route, setRoute] = useState10("boot");
1044
- const [session, setSession] = useState10();
1045
- const [profiles, setProfiles] = useState10([]);
1046
- const [loginTarget, setLoginTarget] = useState10();
1047
- const [editingServer, setEditingServer] = useState10();
1048
- const [busy, setBusy] = useState10(false);
1049
- const [error, setError] = useState10();
1422
+ const [route, setRoute] = useState11("boot");
1423
+ const [session, setSession] = useState11();
1424
+ const [profiles, setProfiles] = useState11([]);
1425
+ const [loginTarget, setLoginTarget] = useState11();
1426
+ const [editingServer, setEditingServer] = useState11();
1427
+ const [busy, setBusy] = useState11(false);
1428
+ const [error, setError] = useState11();
1050
1429
  useInput9((input, key) => {
1051
1430
  if (key.ctrl && input === "q") exit();
1052
1431
  });
@@ -1097,7 +1476,7 @@ function App({ engine }) {
1097
1476
  },
1098
1477
  [engine]
1099
1478
  );
1100
- useEffect7(() => {
1479
+ useEffect8(() => {
1101
1480
  void bootstrap();
1102
1481
  }, [bootstrap]);
1103
1482
  const configure = async (serverUrl) => {
@@ -1230,6 +1609,7 @@ function App({ engine }) {
1230
1609
  return /* @__PURE__ */ jsx10(
1231
1610
  Dashboard,
1232
1611
  {
1612
+ preferences,
1233
1613
  engine,
1234
1614
  session,
1235
1615
  onProfiles: () => void openProfiles(),
@@ -1290,6 +1670,35 @@ function createScreenGuard(stream) {
1290
1670
  };
1291
1671
  }
1292
1672
 
1673
+ // src/tui/preferences.ts
1674
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
1675
+ import { dirname, join } from "node:path";
1676
+ function preferencesPath(env = process.env) {
1677
+ return join(dataDirectory(env), "tui.json");
1678
+ }
1679
+ function localeDefaultHangul(env = process.env) {
1680
+ const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
1681
+ return /^ko(_|-|\.|$)/i.test(locale.trim());
1682
+ }
1683
+ async function readPreferences(env = process.env) {
1684
+ const fallback = { hangulMode: localeDefaultHangul(env) };
1685
+ try {
1686
+ const raw = JSON.parse(await readFile(preferencesPath(env), "utf8"));
1687
+ return typeof raw.hangulMode === "boolean" ? { hangulMode: raw.hangulMode } : fallback;
1688
+ } catch {
1689
+ return fallback;
1690
+ }
1691
+ }
1692
+ async function writePreferences(preferences, env = process.env) {
1693
+ try {
1694
+ const path = preferencesPath(env);
1695
+ await mkdir(dirname(path), { recursive: true });
1696
+ await writeFile(path, `${JSON.stringify(preferences, null, 2)}
1697
+ `, "utf8");
1698
+ } catch {
1699
+ }
1700
+ }
1701
+
1293
1702
  // src/tui/index.tsx
1294
1703
  import { jsx as jsx11 } from "react/jsx-runtime";
1295
1704
  async function runTui(engine) {
@@ -1302,12 +1711,25 @@ async function runTui(engine) {
1302
1711
  process.once("exit", restore);
1303
1712
  process.once("SIGINT", onSignal);
1304
1713
  process.once("SIGTERM", onSignal);
1714
+ const preferences = await readPreferences();
1305
1715
  screen.enter();
1306
1716
  try {
1307
- const instance = render(/* @__PURE__ */ jsx11(App, { engine }), {
1308
- exitOnCtrlC: true,
1309
- patchConsole: true
1310
- });
1717
+ const instance = render(
1718
+ /* @__PURE__ */ jsx11(
1719
+ App,
1720
+ {
1721
+ engine,
1722
+ preferences: {
1723
+ hangulMode: preferences.hangulMode,
1724
+ onHangulModeChange: (enabled) => void writePreferences({ hangulMode: enabled })
1725
+ }
1726
+ }
1727
+ ),
1728
+ {
1729
+ exitOnCtrlC: true,
1730
+ patchConsole: true
1731
+ }
1732
+ );
1311
1733
  await instance.waitUntilExit();
1312
1734
  } finally {
1313
1735
  restore();
@@ -1319,4 +1741,4 @@ async function runTui(engine) {
1319
1741
  export {
1320
1742
  runTui
1321
1743
  };
1322
- //# sourceMappingURL=tui-HKY3YBAY.js.map
1744
+ //# sourceMappingURL=tui-DTMX2JZ7.js.map