@aiwayds/dsh-opentui 0.2.0 → 0.3.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 (2) hide show
  1. package/dist-host/index.js +410 -97
  2. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  // src/host-entry.tsx
2
- import { createComponent as _$createComponent6 } from "@opentui/solid";
2
+ import { createComponent as _$createComponent7 } from "@opentui/solid";
3
3
  import { render } from "@opentui/solid";
4
4
 
5
5
  // ../runtime/src/engine.ts
@@ -91,6 +91,12 @@ var dict = {
91
91
  "cmd.model.available": { "zh-CN": "可用模型", en: "available" },
92
92
  "cmd.model.set": { "zh-CN": "已切换", en: "switched to" },
93
93
  "cmd.model.next": { "zh-CN": "(下一条消息生效)", en: "(next message)" },
94
+ "picker.sessions.title": { "zh-CN": "会话(回车恢复 · esc 取消)", en: "sessions (enter resume · esc cancel)" },
95
+ "picker.effort.title": { "zh-CN": "{model} 思考档位", en: "{model} reasoning effort" },
96
+ "cmd.new.done": { "zh-CN": "已开新会话", en: "new session started" },
97
+ "cmd.sessions.empty": { "zh-CN": "没有可恢复的会话", en: "no stored sessions" },
98
+ "cmd.sessions.resumed": { "zh-CN": "已挂载会话(下条消息生效)", en: "session attached (applies to next message)" },
99
+ "ask.title": { "zh-CN": "模型提问", en: "model asks" },
94
100
  "cmd.usage.summary": { "zh-CN": "累计", en: "total" },
95
101
  "cmd.cleared": { "zh-CN": "会话已清空", en: "session cleared" },
96
102
  "cmd.login.saved": { "zh-CN": "凭据 {ref} 已保存", en: "credential {ref} saved" },
@@ -123,14 +129,14 @@ function createI18n(locale = "zh-CN") {
123
129
 
124
130
  // src/ShellHost.tsx
125
131
  import { createTextNode as _$createTextNode5 } from "@opentui/solid";
126
- import { effect as _$effect5 } from "@opentui/solid";
127
- import { insertNode as _$insertNode5 } from "@opentui/solid";
132
+ import { effect as _$effect6 } from "@opentui/solid";
133
+ import { insertNode as _$insertNode6 } from "@opentui/solid";
128
134
  import { memo as _$memo4 } from "@opentui/solid";
129
- import { insert as _$insert5 } from "@opentui/solid";
130
- import { createComponent as _$createComponent5 } from "@opentui/solid";
131
- import { setProp as _$setProp5 } from "@opentui/solid";
132
- import { createElement as _$createElement5 } from "@opentui/solid";
133
- import { For as For5, createMemo, createSignal, onCleanup, onMount } from "solid-js";
135
+ import { insert as _$insert6 } from "@opentui/solid";
136
+ import { createComponent as _$createComponent6 } from "@opentui/solid";
137
+ import { setProp as _$setProp6 } from "@opentui/solid";
138
+ import { createElement as _$createElement6 } from "@opentui/solid";
139
+ import { For as For6, createMemo, createSignal, onCleanup, onMount } from "solid-js";
134
140
  import { SyntaxStyle } from "@opentui/core";
135
141
  import { useKeyboard, useRenderer } from "@opentui/solid";
136
142
  // ../kit/src/theme.ts
@@ -290,6 +296,48 @@ function renderAssistant(text, theme) {
290
296
  }
291
297
  return out;
292
298
  }
299
+ function itemsToLines(items, theme, t, spinnerFrame, _mdStyle) {
300
+ const out = [];
301
+ for (const it of items) {
302
+ if (it.kind === "user") {
303
+ out.push({
304
+ text: `❯ ${it.text}`,
305
+ fg: theme.tokens.userMark ?? theme.tokens.accent
306
+ });
307
+ } else if (it.kind === "assistant") {
308
+ out.push(...renderAssistant(it.text, theme));
309
+ if (it.final)
310
+ out.push({
311
+ text: ""
312
+ });
313
+ } else if (it.kind === "thinking") {
314
+ out.push({
315
+ text: `${spinnerFrame} ${t("agent.thinking")} (${it.chars}) ${it.tail.slice(-120)}`,
316
+ fg: theme.tokens.dim
317
+ });
318
+ } else if (it.kind === "tool") {
319
+ out.push({
320
+ text: ` ⚒ ${it.name} ${it.args.slice(0, 90)}`,
321
+ fg: theme.tokens.toolOk ?? theme.tokens.warn
322
+ });
323
+ out.push({
324
+ text: ` ⎿ ${it.ok ? t("agent.tool.ok") : t("agent.tool.fail")}: ${it.brief.slice(0, 110)}`,
325
+ fg: it.ok ? theme.tokens.dim : theme.tokens.toolFail ?? theme.tokens.danger
326
+ });
327
+ } else if (it.kind === "error") {
328
+ out.push({
329
+ text: `✗ ${t("agent.error")}: ${it.text}`,
330
+ fg: theme.tokens.danger
331
+ });
332
+ } else {
333
+ out.push({
334
+ text: `· ${it.text}`,
335
+ fg: theme.tokens.dim
336
+ });
337
+ }
338
+ }
339
+ return out;
340
+ }
293
341
  function Transcript(props) {
294
342
  const visibleItems = () => props.items.slice(-(props.maxItems ?? MAX_VISIBLE_ITEMS));
295
343
  const itemNode = (it) => {
@@ -434,6 +482,66 @@ function Transcript(props) {
434
482
  return _el$16;
435
483
  })();
436
484
  }
485
+ function TranscriptWindow(props) {
486
+ const offset = () => props.offset ?? 0;
487
+ if (offset() === 0) {
488
+ return _$createComponent2(Transcript, {
489
+ get items() {
490
+ return props.items;
491
+ },
492
+ get theme() {
493
+ return props.theme;
494
+ },
495
+ get t() {
496
+ return props.t;
497
+ },
498
+ get spinnerFrame() {
499
+ return props.spinnerFrame;
500
+ },
501
+ get maxItems() {
502
+ return props.maxItems;
503
+ },
504
+ get mdStyle() {
505
+ return props.mdStyle;
506
+ }
507
+ });
508
+ }
509
+ const windowLines = () => {
510
+ const visible = props.items.slice(-(props.maxItems ?? MAX_VISIBLE_ITEMS));
511
+ const all = itemsToLines(visible, props.theme, props.t, props.spinnerFrame, props.mdStyle);
512
+ const rows = props.viewportRows ?? 30;
513
+ const start = Math.max(0, all.length - rows - offset());
514
+ const end = Math.max(1, all.length - offset());
515
+ return all.slice(start, end);
516
+ };
517
+ return (() => {
518
+ var _el$18 = _$createElement2("box"), _el$19 = _$createElement2("text");
519
+ _$insertNode2(_el$18, _el$19);
520
+ _$setProp2(_el$18, "style", {
521
+ flexDirection: "column",
522
+ flexGrow: 1,
523
+ padding: 1
524
+ });
525
+ _$insert2(_el$18, _$createComponent2(For2, {
526
+ get each() {
527
+ return windowLines();
528
+ },
529
+ children: (l) => (() => {
530
+ var _el$20 = _$createElement2("text");
531
+ _$insert2(_el$20, () => l.text || " ");
532
+ _$effect2((_$p) => _$setProp2(_el$20, "style", l.fg ? {
533
+ fg: l.fg
534
+ } : {}, _$p));
535
+ return _el$20;
536
+ })()
537
+ }), _el$19);
538
+ _$insert2(_el$19, () => `↑ scrolled ${offset()} rows (PgDn = bottom)`);
539
+ _$effect2((_$p) => _$setProp2(_el$19, "style", {
540
+ fg: props.theme.tokens.dim
541
+ }, _$p));
542
+ return _el$18;
543
+ })();
544
+ }
437
545
  function InputLine(props) {
438
546
  const inputChunks = () => {
439
547
  const s = props.value;
@@ -445,62 +553,62 @@ function InputLine(props) {
445
553
  };
446
554
  };
447
555
  return (() => {
448
- var _el$18 = _$createElement2("box"), _el$19 = _$createElement2("text");
449
- _$insertNode2(_el$18, _el$19);
450
- _$insert2(_el$19, (() => {
556
+ var _el$21 = _$createElement2("box"), _el$22 = _$createElement2("text");
557
+ _$insertNode2(_el$21, _el$22);
558
+ _$insert2(_el$22, (() => {
451
559
  var _c$ = _$memo(() => !!props.busy);
452
560
  return () => _c$() ? `${props.spinnerFrame} ` : "❯ ";
453
561
  })());
454
- _$insert2(_el$18, (() => {
562
+ _$insert2(_el$21, (() => {
455
563
  var _c$2 = _$memo(() => !!props.busy);
456
564
  return () => _c$2() ? (() => {
457
- var _el$20 = _$createElement2("text");
458
- _$insert2(_el$20, () => props.busyLabel);
459
- _$effect2((_$p) => _$setProp2(_el$20, "style", {
565
+ var _el$23 = _$createElement2("text");
566
+ _$insert2(_el$23, () => props.busyLabel);
567
+ _$effect2((_$p) => _$setProp2(_el$23, "style", {
460
568
  fg: props.theme.tokens.dim
461
569
  }, _$p));
462
- return _el$20;
570
+ return _el$23;
463
571
  })() : _$memo(() => props.value === "")() ? [(() => {
464
- var _el$21 = _$createElement2("text");
465
- _$insertNode2(_el$21, _$createTextNode2(` `));
466
- _$effect2((_$p) => _$setProp2(_el$21, "style", {
572
+ var _el$24 = _$createElement2("text");
573
+ _$insertNode2(_el$24, _$createTextNode2(` `));
574
+ _$effect2((_$p) => _$setProp2(_el$24, "style", {
467
575
  fg: props.theme.tokens.accent,
468
576
  attributes: createTextAttributes({
469
577
  inverse: true
470
578
  })
471
579
  }, _$p));
472
- return _el$21;
580
+ return _el$24;
473
581
  })(), (() => {
474
- var _el$23 = _$createElement2("text");
475
- _$insert2(_el$23, () => props.placeholder);
476
- _$effect2((_$p) => _$setProp2(_el$23, "style", {
582
+ var _el$26 = _$createElement2("text");
583
+ _$insert2(_el$26, () => props.placeholder);
584
+ _$effect2((_$p) => _$setProp2(_el$26, "style", {
477
585
  fg: props.theme.tokens.dim
478
586
  }, _$p));
479
- return _el$23;
587
+ return _el$26;
480
588
  })()] : [(() => {
481
- var _el$24 = _$createElement2("text");
482
- _$insert2(_el$24, () => inputChunks().before);
483
- _$effect2((_$p) => _$setProp2(_el$24, "style", {
589
+ var _el$27 = _$createElement2("text");
590
+ _$insert2(_el$27, () => inputChunks().before);
591
+ _$effect2((_$p) => _$setProp2(_el$27, "style", {
484
592
  fg: props.theme.tokens.text
485
593
  }, _$p));
486
- return _el$24;
594
+ return _el$27;
487
595
  })(), (() => {
488
- var _el$25 = _$createElement2("text");
489
- _$insert2(_el$25, () => inputChunks().at);
490
- _$effect2((_$p) => _$setProp2(_el$25, "style", {
596
+ var _el$28 = _$createElement2("text");
597
+ _$insert2(_el$28, () => inputChunks().at);
598
+ _$effect2((_$p) => _$setProp2(_el$28, "style", {
491
599
  fg: props.theme.tokens.text,
492
600
  attributes: createTextAttributes({
493
601
  inverse: true
494
602
  })
495
603
  }, _$p));
496
- return _el$25;
604
+ return _el$28;
497
605
  })(), (() => {
498
- var _el$26 = _$createElement2("text");
499
- _$insert2(_el$26, () => inputChunks().after);
500
- _$effect2((_$p) => _$setProp2(_el$26, "style", {
606
+ var _el$29 = _$createElement2("text");
607
+ _$insert2(_el$29, () => inputChunks().after);
608
+ _$effect2((_$p) => _$setProp2(_el$29, "style", {
501
609
  fg: props.theme.tokens.text
502
610
  }, _$p));
503
- return _el$26;
611
+ return _el$29;
504
612
  })()];
505
613
  })(), null);
506
614
  _$effect2((_p$) => {
@@ -511,39 +619,39 @@ function InputLine(props) {
511
619
  }, _v$6 = {
512
620
  fg: props.theme.tokens.accent
513
621
  };
514
- _v$5 !== _p$.e && (_p$.e = _$setProp2(_el$18, "style", _v$5, _p$.e));
515
- _v$6 !== _p$.t && (_p$.t = _$setProp2(_el$19, "style", _v$6, _p$.t));
622
+ _v$5 !== _p$.e && (_p$.e = _$setProp2(_el$21, "style", _v$5, _p$.e));
623
+ _v$6 !== _p$.t && (_p$.t = _$setProp2(_el$22, "style", _v$6, _p$.t));
516
624
  return _p$;
517
625
  }, {
518
626
  e: undefined,
519
627
  t: undefined
520
628
  });
521
- return _el$18;
629
+ return _el$21;
522
630
  })();
523
631
  }
524
632
  function FooterHints(props) {
525
633
  return (() => {
526
- var _el$27 = _$createElement2("box");
527
- _$setProp2(_el$27, "style", {
634
+ var _el$30 = _$createElement2("box");
635
+ _$setProp2(_el$30, "style", {
528
636
  flexDirection: "row",
529
637
  paddingLeft: 2,
530
638
  paddingRight: 2
531
639
  });
532
- _$insert2(_el$27, _$createComponent2(For2, {
640
+ _$insert2(_el$30, _$createComponent2(For2, {
533
641
  each: footerHints,
534
642
  children: (h, i) => (() => {
535
- var _el$28 = _$createElement2("text"), _el$29 = _$createTextNode2(` `);
536
- _$insertNode2(_el$28, _el$29);
537
- _$insert2(_el$28, () => i() > 0 ? " · " : "", _el$29);
538
- _$insert2(_el$28, () => h.key, _el$29);
539
- _$insert2(_el$28, () => h.label, null);
540
- _$effect2((_$p) => _$setProp2(_el$28, "style", {
643
+ var _el$31 = _$createElement2("text"), _el$32 = _$createTextNode2(` `);
644
+ _$insertNode2(_el$31, _el$32);
645
+ _$insert2(_el$31, () => i() > 0 ? " · " : "", _el$32);
646
+ _$insert2(_el$31, () => h.key, _el$32);
647
+ _$insert2(_el$31, () => h.label, null);
648
+ _$effect2((_$p) => _$setProp2(_el$31, "style", {
541
649
  fg: props.theme.tokens.dim
542
650
  }, _$p));
543
- return _el$28;
651
+ return _el$31;
544
652
  })()
545
653
  }));
546
- return _el$27;
654
+ return _el$30;
547
655
  })();
548
656
  }
549
657
  // ../kit/src/overlays.tsx
@@ -813,6 +921,14 @@ function ThinkPanel(props) {
813
921
  return _el$7;
814
922
  })();
815
923
  }
924
+ // ../kit/src/table.tsx
925
+ import { effect as _$effect5 } from "@opentui/solid";
926
+ import { insertNode as _$insertNode5 } from "@opentui/solid";
927
+ import { createComponent as _$createComponent5 } from "@opentui/solid";
928
+ import { insert as _$insert5 } from "@opentui/solid";
929
+ import { setProp as _$setProp5 } from "@opentui/solid";
930
+ import { createElement as _$createElement5 } from "@opentui/solid";
931
+ import { For as For5 } from "solid-js";
816
932
  // src/ShellHost.tsx
817
933
  var MAX_VISIBLE_ITEMS2 = 120;
818
934
  var THEME_ORDER = ["catppuccin-mocha", "default"];
@@ -826,6 +942,12 @@ var COMMANDS = [{
826
942
  }, {
827
943
  name: "/usage",
828
944
  desc: "show token usage summary"
945
+ }, {
946
+ name: "/new",
947
+ desc: "start a fresh session"
948
+ }, {
949
+ name: "/sessions",
950
+ desc: "list and resume stored sessions"
829
951
  }, {
830
952
  name: "/theme",
831
953
  desc: "cycle theme"
@@ -866,6 +988,9 @@ function ShellHost(props) {
866
988
  const [toolExpanded, setToolExpanded] = createSignal(-1);
867
989
  const [picker, setPicker] = createSignal(null);
868
990
  const [pickerSel, setPickerSel] = createSignal(0);
991
+ const [sessionItems, setSessionItems] = createSignal([]);
992
+ const [scrollOffset, setScrollOffset] = createSignal(0);
993
+ const [ask, setAsk] = createSignal(null);
869
994
  let reasoningBuf = "";
870
995
  const [lastReasoning, setLastReasoning] = createSignal("");
871
996
  const initialTheme = process.env.DSH_OPENTUI_THEME ?? props.deps.themeName ?? "catppuccin-mocha";
@@ -878,6 +1003,17 @@ function ShellHost(props) {
878
1003
  onCleanup(store.subscribe(() => setSnapshot(store.snapshot)));
879
1004
  const t2 = setInterval(() => setSpin((s) => (s + 1) % 4), 150);
880
1005
  onCleanup(() => clearInterval(t2));
1006
+ props.deps.session.registerAskProvider(async (questions) => {
1007
+ const answer = await new Promise((resolve) => {
1008
+ setAsk({
1009
+ questions,
1010
+ resolve
1011
+ });
1012
+ });
1013
+ if (answer === "declined")
1014
+ throw new Error("declined");
1015
+ return answer;
1016
+ });
881
1017
  });
882
1018
  const spinner = () => ["⠋", "⠙", "⠹", "⠸"][spin()];
883
1019
  const filteredCommands = createMemo(() => {
@@ -891,7 +1027,7 @@ function ShellHost(props) {
891
1027
  return n === 0 ? 0 : Math.min(menuSel(), n - 1);
892
1028
  });
893
1029
  const menuVisible = createMemo(() => !busy() && menuOpen() && filteredCommands().length > 0);
894
- const pickerItems = createMemo(() => {
1030
+ const modelItems = createMemo(() => {
895
1031
  const out = [];
896
1032
  for (const p of props.deps.session.listProviders()) {
897
1033
  for (const m of p.models) {
@@ -906,6 +1042,34 @@ function ShellHost(props) {
906
1042
  }
907
1043
  return out;
908
1044
  });
1045
+ const EFFORTS = ["keep", "low", "medium", "high", "xhigh", "max"];
1046
+ const pickerData = createMemo(() => {
1047
+ const p = picker();
1048
+ if (!p)
1049
+ return {
1050
+ title: "",
1051
+ items: []
1052
+ };
1053
+ if (p.kind === "model")
1054
+ return {
1055
+ title: t("picker.model.title"),
1056
+ items: modelItems()
1057
+ };
1058
+ if (p.kind === "sessions")
1059
+ return {
1060
+ title: t("picker.sessions.title"),
1061
+ items: sessionItems()
1062
+ };
1063
+ return {
1064
+ title: t("picker.effort.title", {
1065
+ model: p.model
1066
+ }),
1067
+ items: EFFORTS.map((e) => ({
1068
+ id: e,
1069
+ label: e
1070
+ }))
1071
+ };
1072
+ });
909
1073
  const toolEntries = createMemo(() => items().filter((it) => it.kind === "tool").map((it) => ({
910
1074
  name: it.name,
911
1075
  args: it.args,
@@ -1010,6 +1174,32 @@ function ShellHost(props) {
1010
1174
  kind: "system",
1011
1175
  text: `${t("cmd.usage.summary")}: ${u.turns} turns · prompt ${u.prompt} · completion ${u.completion} tokens`
1012
1176
  });
1177
+ } else if (cmd === "new") {
1178
+ props.deps.session.newSession();
1179
+ setItems([]);
1180
+ setScrollOffset(0);
1181
+ push({
1182
+ kind: "system",
1183
+ text: t("cmd.new.done")
1184
+ });
1185
+ } else if (cmd === "sessions") {
1186
+ const list = await props.deps.session.listSessions();
1187
+ if (!list.length) {
1188
+ push({
1189
+ kind: "system",
1190
+ text: t("cmd.sessions.empty")
1191
+ });
1192
+ } else {
1193
+ setSessionItems(list.map((x) => ({
1194
+ id: x.id,
1195
+ label: x.title || x.id.slice(0, 8),
1196
+ detail: x.updatedAt ? String(x.updatedAt).slice(0, 19) : undefined
1197
+ })));
1198
+ setPickerSel(0);
1199
+ setPicker({
1200
+ kind: "sessions"
1201
+ });
1202
+ }
1013
1203
  } else if (cmd === "theme") {
1014
1204
  const i = THEME_ORDER.indexOf(themeName());
1015
1205
  const next = THEME_ORDER[(i + 1) % THEME_ORDER.length];
@@ -1098,6 +1288,7 @@ function ShellHost(props) {
1098
1288
  async function send(text) {
1099
1289
  if (busy())
1100
1290
  return;
1291
+ setScrollOffset(0);
1101
1292
  push({
1102
1293
  kind: "user",
1103
1294
  text
@@ -1204,6 +1395,26 @@ function ShellHost(props) {
1204
1395
  }
1205
1396
  if (key.sequence === "\x03" || (key.name === "c" || key.name === "C") && key.ctrl)
1206
1397
  process.exit(0);
1398
+ const pendingAsk = ask();
1399
+ if (pendingAsk) {
1400
+ const q0 = pendingAsk.questions[0];
1401
+ const opts = q0?.options ?? [];
1402
+ if (key.name === "up" || key.name === "down") {
1403
+ if (opts.length > 0)
1404
+ setPickerSel((v) => key.name === "up" ? (v - 1 + opts.length) % opts.length : (v + 1) % opts.length);
1405
+ } else if (key.name === "return") {
1406
+ const sel = opts[Math.min(pickerSel(), Math.max(0, opts.length - 1))];
1407
+ setAsk(null);
1408
+ pendingAsk.resolve(sel ? [{
1409
+ questionId: q0.id,
1410
+ selected: sel
1411
+ }] : "declined");
1412
+ } else if (key.name === "escape") {
1413
+ setAsk(null);
1414
+ pendingAsk.resolve("declined");
1415
+ }
1416
+ return;
1417
+ }
1207
1418
  const ap = approval();
1208
1419
  if (ap) {
1209
1420
  if (key.sequence === "y" || key.name === "return") {
@@ -1217,25 +1428,38 @@ function ShellHost(props) {
1217
1428
  }
1218
1429
  return;
1219
1430
  }
1220
- if (picker()) {
1221
- const n = pickerItems().length;
1431
+ const pk = picker();
1432
+ if (pk) {
1433
+ const n = pickerData().items.length;
1222
1434
  if (key.name === "up" || key.name === "down") {
1223
1435
  if (n > 0)
1224
1436
  setPickerSel((s) => key.name === "up" ? (s - 1 + n) % n : (s + 1) % n);
1225
1437
  } else if (key.name === "return") {
1226
- const item = n > 0 ? pickerItems()[Math.min(pickerSel(), n - 1)] : undefined;
1438
+ const item = n > 0 ? pickerData().items[Math.min(pickerSel(), n - 1)] : undefined;
1227
1439
  setPicker(null);
1228
1440
  setPickerSel(0);
1229
- if (item) {
1441
+ if (item && pk.kind === "model") {
1230
1442
  const [provider, model] = item.id.split("/");
1231
- if (provider && model) {
1232
- props.deps.session.switchModel(provider, model);
1233
- setLabel(props.deps.session.label);
1234
- push({
1235
- kind: "system",
1236
- text: `${t("cmd.model.set")} ${item.id} ${t("cmd.model.next")}`
1443
+ if (provider && model)
1444
+ setPicker({
1445
+ kind: "effort",
1446
+ provider,
1447
+ model
1237
1448
  });
1238
- }
1449
+ } else if (item && pk.kind === "effort") {
1450
+ const effort = item.id === "keep" ? undefined : item.id;
1451
+ props.deps.session.switchModel(pk.provider, pk.model, effort);
1452
+ setLabel(props.deps.session.label);
1453
+ push({
1454
+ kind: "system",
1455
+ text: `${t("cmd.model.set")} ${pk.provider}/${pk.model}${effort ? ` (${effort})` : ""} ${t("cmd.model.next")}`
1456
+ });
1457
+ } else if (item && pk.kind === "sessions") {
1458
+ props.deps.session.resumeSession(item.id);
1459
+ push({
1460
+ kind: "system",
1461
+ text: `${t("cmd.sessions.resumed")}: ${item.label}`
1462
+ });
1239
1463
  }
1240
1464
  } else if (key.name === "escape") {
1241
1465
  setPicker(null);
@@ -1257,6 +1481,14 @@ function ShellHost(props) {
1257
1481
  setToolExpanded((v) => v + 1 >= n ? -1 : v + 1);
1258
1482
  return;
1259
1483
  }
1484
+ if (key.name === "pageup") {
1485
+ setScrollOffset((o) => o + 10);
1486
+ return;
1487
+ }
1488
+ if (key.name === "pagedown") {
1489
+ setScrollOffset((o) => Math.max(0, o - 10));
1490
+ return;
1491
+ }
1260
1492
  if (busy())
1261
1493
  return;
1262
1494
  if (menuVisible() && (key.name === "up" || key.name === "down")) {
@@ -1370,18 +1602,18 @@ function ShellHost(props) {
1370
1602
  const widgets = () => snapshot().filter((d) => d.kind === "widget");
1371
1603
  const th = () => theme();
1372
1604
  return (() => {
1373
- var _el$ = _$createElement5("box"), _el$2 = _$createElement5("box");
1374
- _$insertNode5(_el$, _el$2);
1375
- _$setProp5(_el$, "style", {
1605
+ var _el$ = _$createElement6("box"), _el$2 = _$createElement6("box");
1606
+ _$insertNode6(_el$, _el$2);
1607
+ _$setProp6(_el$, "style", {
1376
1608
  flexDirection: "column",
1377
1609
  flexGrow: 1
1378
1610
  });
1379
- _$setProp5(_el$2, "style", {
1611
+ _$setProp6(_el$2, "style", {
1380
1612
  flexDirection: "column",
1381
1613
  flexGrow: 1,
1382
1614
  padding: 1
1383
1615
  });
1384
- _$insert5(_el$2, _$createComponent5(Transcript, {
1616
+ _$insert6(_el$2, _$createComponent6(TranscriptWindow, {
1385
1617
  get items() {
1386
1618
  return visibleItems();
1387
1619
  },
@@ -1392,11 +1624,15 @@ function ShellHost(props) {
1392
1624
  get spinnerFrame() {
1393
1625
  return spinner();
1394
1626
  },
1395
- mdStyle
1627
+ mdStyle,
1628
+ get offset() {
1629
+ return scrollOffset();
1630
+ },
1631
+ viewportRows: 24
1396
1632
  }));
1397
- _$insert5(_el$, (() => {
1633
+ _$insert6(_el$, (() => {
1398
1634
  var _c$ = _$memo4(() => !!showTool());
1399
- return () => _c$() ? _$createComponent5(ToolPanel, {
1635
+ return () => _c$() ? _$createComponent6(ToolPanel, {
1400
1636
  get entries() {
1401
1637
  return toolEntries();
1402
1638
  },
@@ -1408,9 +1644,9 @@ function ShellHost(props) {
1408
1644
  }
1409
1645
  }) : null;
1410
1646
  })(), null);
1411
- _$insert5(_el$, (() => {
1647
+ _$insert6(_el$, (() => {
1412
1648
  var _c$2 = _$memo4(() => !!showThink());
1413
- return () => _c$2() ? _$createComponent5(ThinkPanel, {
1649
+ return () => _c$2() ? _$createComponent6(ThinkPanel, {
1414
1650
  get text() {
1415
1651
  return lastReasoning();
1416
1652
  },
@@ -1419,11 +1655,11 @@ function ShellHost(props) {
1419
1655
  }
1420
1656
  }) : null;
1421
1657
  })(), null);
1422
- _$insert5(_el$, _$createComponent5(For5, {
1658
+ _$insert6(_el$, _$createComponent6(For6, {
1423
1659
  get each() {
1424
1660
  return _$memo4(() => !!approval())() ? [approval()] : [];
1425
1661
  },
1426
- children: (ap) => _$createComponent5(ApprovalCard, {
1662
+ children: (ap) => _$createComponent6(ApprovalCard, {
1427
1663
  get toolName() {
1428
1664
  return ap.name;
1429
1665
  },
@@ -1438,14 +1674,35 @@ function ShellHost(props) {
1438
1674
  }
1439
1675
  })
1440
1676
  }), null);
1441
- _$insert5(_el$, (() => {
1442
- var _c$3 = _$memo4(() => !!picker());
1443
- return () => _c$3() ? _$createComponent5(Picker, {
1677
+ _$insert6(_el$, (() => {
1678
+ var _c$3 = _$memo4(() => !!ask());
1679
+ return () => _c$3() ? _$createComponent6(Picker, {
1680
+ get title() {
1681
+ return `${t("ask.title")}: ${ask().questions[0]?.prompt ?? ""}`.slice(0, 70);
1682
+ },
1683
+ get items() {
1684
+ return (ask().questions[0]?.options ?? []).map((o) => ({
1685
+ id: o,
1686
+ label: o
1687
+ }));
1688
+ },
1689
+ get selected() {
1690
+ return pickerSel();
1691
+ },
1692
+ get theme() {
1693
+ return th();
1694
+ },
1695
+ maxRows: 8
1696
+ }) : null;
1697
+ })(), null);
1698
+ _$insert6(_el$, (() => {
1699
+ var _c$4 = _$memo4(() => !!picker());
1700
+ return () => _c$4() ? _$createComponent6(Picker, {
1444
1701
  get title() {
1445
- return t("picker.model.title");
1702
+ return pickerData().title;
1446
1703
  },
1447
1704
  get items() {
1448
- return pickerItems();
1705
+ return pickerData().items;
1449
1706
  },
1450
1707
  get selected() {
1451
1708
  return pickerSel();
@@ -1456,29 +1713,29 @@ function ShellHost(props) {
1456
1713
  maxRows: 8
1457
1714
  }) : null;
1458
1715
  })(), null);
1459
- _$insert5(_el$, _$createComponent5(For5, {
1716
+ _$insert6(_el$, _$createComponent6(For6, {
1460
1717
  get each() {
1461
1718
  return menuVisible() ? [0] : [];
1462
1719
  },
1463
1720
  children: () => (() => {
1464
- var _el$3 = _$createElement5("box");
1465
- _$insert5(_el$3, _$createComponent5(For5, {
1721
+ var _el$3 = _$createElement6("box");
1722
+ _$insert6(_el$3, _$createComponent6(For6, {
1466
1723
  get each() {
1467
1724
  return filteredCommands();
1468
1725
  },
1469
1726
  children: (c, i) => (() => {
1470
- var _el$4 = _$createElement5("text"), _el$5 = _$createTextNode5(` `);
1471
- _$insertNode5(_el$4, _el$5);
1472
- _$insert5(_el$4, () => i() === clampedSel() ? "› " : " ", _el$5);
1473
- _$insert5(_el$4, () => c.name, _el$5);
1474
- _$insert5(_el$4, () => c.desc, null);
1475
- _$effect5((_$p) => _$setProp5(_el$4, "style", {
1727
+ var _el$4 = _$createElement6("text"), _el$5 = _$createTextNode5(` `);
1728
+ _$insertNode6(_el$4, _el$5);
1729
+ _$insert6(_el$4, () => i() === clampedSel() ? "› " : " ", _el$5);
1730
+ _$insert6(_el$4, () => c.name, _el$5);
1731
+ _$insert6(_el$4, () => c.desc, null);
1732
+ _$effect6((_$p) => _$setProp6(_el$4, "style", {
1476
1733
  fg: i() === clampedSel() ? th().tokens.accent : undefined
1477
1734
  }, _$p));
1478
1735
  return _el$4;
1479
1736
  })()
1480
1737
  }));
1481
- _$effect5((_$p) => _$setProp5(_el$3, "style", {
1738
+ _$effect6((_$p) => _$setProp6(_el$3, "style", {
1482
1739
  border: true,
1483
1740
  borderColor: th().tokens.border,
1484
1741
  flexDirection: "column",
@@ -1487,7 +1744,7 @@ function ShellHost(props) {
1487
1744
  return _el$3;
1488
1745
  })()
1489
1746
  }), null);
1490
- _$insert5(_el$, _$createComponent5(InputLine, {
1747
+ _$insert6(_el$, _$createComponent6(InputLine, {
1491
1748
  get value() {
1492
1749
  return input();
1493
1750
  },
@@ -1510,12 +1767,12 @@ function ShellHost(props) {
1510
1767
  return th();
1511
1768
  }
1512
1769
  }), null);
1513
- _$insert5(_el$, _$createComponent5(FooterHints, {
1770
+ _$insert6(_el$, _$createComponent6(FooterHints, {
1514
1771
  get theme() {
1515
1772
  return th();
1516
1773
  }
1517
1774
  }), null);
1518
- _$insert5(_el$, _$createComponent5(StatusBar, {
1775
+ _$insert6(_el$, _$createComponent6(StatusBar, {
1519
1776
  get widgets() {
1520
1777
  return [{
1521
1778
  kind: "widget",
@@ -1557,6 +1814,7 @@ class HostSession {
1557
1814
  #running = false;
1558
1815
  #toolCalls = new Map;
1559
1816
  #override;
1817
+ #resumeId;
1560
1818
  usage = { prompt: 0, completion: 0 };
1561
1819
  constructor(ctx) {
1562
1820
  this.#ctx = ctx;
@@ -1574,15 +1832,69 @@ class HostSession {
1574
1832
  return [];
1575
1833
  }
1576
1834
  }
1577
- switchModel(provider, model) {
1578
- this.#override = { provider, model };
1835
+ switchModel(provider, model, reasoningEffort) {
1836
+ this.#override = { provider, model, ...reasoningEffort ? { reasoningEffort } : {} };
1579
1837
  try {
1580
1838
  this.#handle?.dispose?.();
1581
1839
  } catch {}
1582
1840
  this.#handle = undefined;
1583
1841
  this.#sessionId = undefined;
1584
1842
  this.#agentId = undefined;
1585
- this.label = `${provider}/${model}`;
1843
+ this.label = `${provider}/${model}${reasoningEffort ? ` (${reasoningEffort})` : ""}`;
1844
+ }
1845
+ newSession() {
1846
+ try {
1847
+ this.#handle?.dispose?.();
1848
+ } catch {}
1849
+ this.#handle = undefined;
1850
+ this.#sessionId = undefined;
1851
+ this.#agentId = undefined;
1852
+ }
1853
+ resumeSession(id) {
1854
+ this.newSession();
1855
+ this.#resumeId = id;
1856
+ }
1857
+ async listSessions(limit = 12) {
1858
+ const persistence = this.#ctx.get?.("sessionPersistence");
1859
+ if (!persistence?.list)
1860
+ return [];
1861
+ try {
1862
+ const headers = await persistence.list();
1863
+ return headers.map((h) => ({
1864
+ id: String(h?.id ?? ""),
1865
+ title: String(h?.title ?? h?.firstPrompt ?? h?.name ?? "").slice(0, 40),
1866
+ updatedAt: h?.updatedAt ?? h?.lastEventAt ?? h?.mtime
1867
+ })).filter((s) => s.id).sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? ""))).slice(0, limit);
1868
+ } catch {
1869
+ return [];
1870
+ }
1871
+ }
1872
+ registerAskProvider(onAsk) {
1873
+ this.#ctx.on("user-questions/request", async (request, next) => {
1874
+ const asking = request?.agent?.session?.id;
1875
+ if (this.#sessionId !== undefined && asking !== undefined && String(asking) !== this.#sessionId)
1876
+ return next();
1877
+ const questions = request?.questions ?? [];
1878
+ if (!questions.length)
1879
+ return next();
1880
+ const simplified = questions.map((q) => ({
1881
+ id: String(q?.id ?? ""),
1882
+ prompt: String(q?.prompt ?? q?.question ?? q?.header ?? ""),
1883
+ options: (q?.options ?? []).map((o) => String(o?.label ?? o?.value ?? o))
1884
+ }));
1885
+ let picks;
1886
+ try {
1887
+ picks = await onAsk(simplified);
1888
+ } catch {
1889
+ return { answers: simplified.map((q) => ({ id: q.id, selected: [], custom: "declined" })) };
1890
+ }
1891
+ return {
1892
+ answers: simplified.map((q) => {
1893
+ const pick = picks.find((p) => p.questionId === q.id);
1894
+ return pick ? { id: q.id, selected: [pick.selected] } : { id: q.id, selected: [], custom: "declined" };
1895
+ })
1896
+ };
1897
+ });
1586
1898
  }
1587
1899
  #credentials() {
1588
1900
  try {
@@ -1612,7 +1924,8 @@ class HostSession {
1612
1924
  async turn(userText, cb, _signal) {
1613
1925
  this.#cb = cb;
1614
1926
  if (!this.#handle) {
1615
- this.#sessionId = randomUUID();
1927
+ this.#sessionId = this.#resumeId ?? randomUUID();
1928
+ this.#resumeId = undefined;
1616
1929
  const sel = this.#override ?? this.#ctx.get?.("agentDefaultModel")?.currentSelection?.();
1617
1930
  const agentOptions = sel ? { provider: sel.provider, model: sel.model, ...sel.reasoningEffort ? { reasoningEffort: sel.reasoningEffort } : {} } : {};
1618
1931
  if (sel)
@@ -1737,7 +2050,7 @@ function apply(ctx, config) {
1737
2050
  mountEngine({}).then(({
1738
2051
  renderer
1739
2052
  }) => {
1740
- render(() => _$createComponent6(ShellHost, {
2053
+ render(() => _$createComponent7(ShellHost, {
1741
2054
  get deps() {
1742
2055
  return {
1743
2056
  store,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwayds/dsh-opentui",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "dsh-opentui": "bin/dsh-opentui.mjs"