tinker-agent 2.6.0 → 2.8.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.
@@ -15,10 +15,16 @@ import {
15
15
  type LineEditorState,
16
16
  } from "../line-editor";
17
17
 
18
- const SESSION_ROWS = 3;
18
+ const SESSION_ROWS = 1;
19
19
  const BROWSE_CHROME_ROWS = 3;
20
20
  const SEARCH_CHROME_ROWS = 4;
21
21
  const MAX_DISPLAYED_SESSIONS = 20;
22
+ const MARKER_COLUMN_WIDTH = 2;
23
+ const TIME_COLUMN_WIDTH = 10;
24
+ const TURN_COLUMN_WIDTH = 11;
25
+ const STATUS_COLUMN_WIDTH = 13;
26
+ const PROFILE_COLUMN_WIDTH = 16;
27
+ const SESSION_ROW_BACKGROUNDS = ["#1c1c1c", "#101010"] as const;
22
28
 
23
29
  export type ResumeSessionPickerProps = {
24
30
  sessions: readonly SessionSummary[];
@@ -26,6 +32,7 @@ export type ResumeSessionPickerProps = {
26
32
  error?: string;
27
33
  now?: Date;
28
34
  viewportRows?: number;
35
+ viewportColumns?: number;
29
36
  visibleItemCount?: number;
30
37
  onCancel: () => void;
31
38
  onSelect: (session: SessionSummary) => void;
@@ -56,6 +63,7 @@ export function ResumeSessionPicker(props: ResumeSessionPickerProps) {
56
63
  function ResumeSessionPickerContent(props: ResumeSessionPickerProps) {
57
64
  const windowSize = useWindowSize();
58
65
  const rows = props.viewportRows ?? windowSize.rows - 1;
66
+ const columns = Math.max(1, props.viewportColumns ?? windowSize.columns);
59
67
 
60
68
  const displayedFor = (value: string): readonly SessionSummary[] => {
61
69
  const nextCandidates =
@@ -287,7 +295,7 @@ function ResumeSessionPickerContent(props: ResumeSessionPickerProps) {
287
295
 
288
296
  const now = props.now ?? new Date();
289
297
  return (
290
- <Box flexDirection="column">
298
+ <Box width={columns} flexDirection="column" overflow="hidden">
291
299
  <Text bold>Resume session</Text>
292
300
  {state.mode === "search" ? <SearchLine editor={state.editor} /> : null}
293
301
  <Text dimColor>
@@ -303,6 +311,8 @@ function ResumeSessionPickerContent(props: ResumeSessionPickerProps) {
303
311
  session={session}
304
312
  isSelected={windowStart + offset === selectedIndex}
305
313
  now={now}
314
+ rowIndex={windowStart + offset}
315
+ columns={columns}
306
316
  />
307
317
  ))}
308
318
  <Text
@@ -318,6 +328,7 @@ function ResumeSessionPickerContent(props: ResumeSessionPickerProps) {
318
328
  windowStart,
319
329
  windowEnd,
320
330
  totalCount: props.sessions.length,
331
+ selectedSession,
321
332
  })
322
333
  : `Resume failed: ${singleLine(props.error)}`}
323
334
  </Text>
@@ -368,38 +379,92 @@ function SessionOption(props: {
368
379
  session: SessionSummary;
369
380
  isSelected: boolean;
370
381
  now: Date;
382
+ rowIndex: number;
383
+ columns: number;
371
384
  }) {
372
385
  const selectable = isSessionSelectable(props.session);
373
386
  const marker = props.isSelected ? "❯ " : " ";
374
387
  const preview =
375
388
  singleLine(props.session.firstUserPromptPreview ?? "") || "(no prompt)";
389
+ const profile = singleLine(props.session.profileName ?? "") || "—";
376
390
 
377
391
  return (
378
- <Box flexDirection="column">
392
+ <Box
393
+ width={props.columns}
394
+ height={SESSION_ROWS}
395
+ overflow="hidden"
396
+ backgroundColor={
397
+ SESSION_ROW_BACKGROUNDS[props.rowIndex % SESSION_ROW_BACKGROUNDS.length]
398
+ }
399
+ >
400
+ <SessionCell
401
+ value={marker}
402
+ width={MARKER_COLUMN_WIDTH}
403
+ isSelected={props.isSelected}
404
+ isDisabled={!selectable}
405
+ />
406
+ <SessionCell
407
+ value={formatRelativeTime(props.session.updatedAt, props.now)}
408
+ width={TIME_COLUMN_WIDTH}
409
+ isSelected={props.isSelected}
410
+ isDisabled={!selectable}
411
+ padRight
412
+ />
413
+ <SessionCell
414
+ value={`${props.session.turnCount} ${props.session.turnCount === 1 ? "turn" : "turns"}`}
415
+ width={TURN_COLUMN_WIDTH}
416
+ isSelected={props.isSelected}
417
+ isDisabled={!selectable}
418
+ padRight
419
+ />
420
+ <SessionCell
421
+ value={sessionStatusLabel(props.session)}
422
+ width={STATUS_COLUMN_WIDTH}
423
+ isSelected={props.isSelected}
424
+ isDisabled={!selectable}
425
+ padRight
426
+ />
427
+ <SessionCell
428
+ value={profile}
429
+ width={PROFILE_COLUMN_WIDTH}
430
+ isSelected={props.isSelected}
431
+ isDisabled={!selectable}
432
+ padRight
433
+ />
434
+ <SessionCell
435
+ value={preview}
436
+ isSelected={props.isSelected}
437
+ isDisabled={!selectable}
438
+ />
439
+ </Box>
440
+ );
441
+ }
442
+
443
+ function SessionCell(props: {
444
+ value: string;
445
+ width?: number;
446
+ isSelected: boolean;
447
+ isDisabled: boolean;
448
+ padRight?: boolean;
449
+ }) {
450
+ const selected = props.isSelected && !props.isDisabled;
451
+ return (
452
+ <Box
453
+ width={props.width}
454
+ minWidth={props.width === undefined ? 0 : undefined}
455
+ flexGrow={props.width === undefined ? 1 : 0}
456
+ flexShrink={props.width === undefined ? 1 : 0}
457
+ paddingRight={props.padRight === true ? 1 : 0}
458
+ overflow="hidden"
459
+ >
379
460
  <Text
380
- color={props.isSelected && selectable ? "cyan" : undefined}
381
- bold={props.isSelected && selectable}
382
- dimColor={!selectable}
461
+ color={selected ? "cyan" : undefined}
462
+ bold={selected}
463
+ dimColor={props.isDisabled}
383
464
  wrap="truncate-end"
384
465
  >
385
- {marker}
386
- {formatRelativeTime(props.session.updatedAt, props.now)} ·{" "}
387
- {props.session.turnCount} {props.session.turnCount === 1 ? "turn" : "turns"} ·{" "}
388
- {sessionStatusText(props.session)}
466
+ {props.value}
389
467
  </Text>
390
- <Box marginLeft={2} overflow="hidden">
391
- <Text dimColor={!selectable} wrap="truncate-end">
392
- {preview}
393
- </Text>
394
- </Box>
395
- <Box marginLeft={2} overflow="hidden">
396
- <Box flexGrow={1} overflow="hidden">
397
- <Text dimColor wrap="truncate-end">
398
- {singleLine(props.session.modelName)}
399
- </Text>
400
- </Box>
401
- <Text dimColor> {shortSessionId(props.session.sessionId)}</Text>
402
- </Box>
403
468
  </Box>
404
469
  );
405
470
  }
@@ -416,20 +481,20 @@ export function formatRelativeTime(updatedAt: string, now: Date): string {
416
481
 
417
482
  const elapsedSeconds = Math.max(0, Math.floor((now.getTime() - timestamp) / 1000));
418
483
  if (elapsedSeconds < 60) {
419
- return "just now";
484
+ return "now";
420
485
  }
421
486
 
422
487
  const elapsedMinutes = Math.floor(elapsedSeconds / 60);
423
488
  if (elapsedMinutes < 60) {
424
- return formatAgo(elapsedMinutes, "minute");
489
+ return `${elapsedMinutes}m ago`;
425
490
  }
426
491
 
427
492
  const elapsedHours = Math.floor(elapsedMinutes / 60);
428
493
  if (elapsedHours < 24) {
429
- return formatAgo(elapsedHours, "hour");
494
+ return `${elapsedHours}h ago`;
430
495
  }
431
496
 
432
- return formatAgo(Math.floor(elapsedHours / 24), "day");
497
+ return `${Math.floor(elapsedHours / 24)}d ago`;
433
498
  }
434
499
 
435
500
  function initialSelectedIndex(sessions: readonly SessionSummary[]): number {
@@ -453,10 +518,14 @@ function keepSelectionVisible(
453
518
  return clamp(nextWindowStart, 0, maxWindowStart);
454
519
  }
455
520
 
456
- function sessionStatusText(session: SessionSummary): string {
521
+ function sessionStatusLabel(session: SessionSummary): string {
522
+ return session.status;
523
+ }
524
+
525
+ function sessionStatusDetail(session: SessionSummary): string | undefined {
457
526
  switch (session.status) {
458
527
  case "resumable":
459
- return "resumable";
528
+ return undefined;
460
529
  case "interrupted":
461
530
  return "interrupted · completes record; no tool retry";
462
531
  case "current":
@@ -481,20 +550,36 @@ function formatFooter(input: {
481
550
  windowStart: number;
482
551
  windowEnd: number;
483
552
  totalCount: number;
553
+ selectedSession?: SessionSummary;
484
554
  }): string {
555
+ const detail =
556
+ input.selectedSession === undefined
557
+ ? undefined
558
+ : sessionStatusDetail(input.selectedSession);
559
+ const withDetail = (status: string) =>
560
+ detail === undefined ? status : `${status} · ${detail}`;
561
+
485
562
  if (input.searching) {
486
563
  if (input.matchCount === 0) {
487
564
  return `No sessions match "${singleLine(input.query)}" · Esc to clear search`;
488
565
  }
489
566
  if (input.matchCount > MAX_DISPLAYED_SESSIONS) {
490
- return `Showing ${input.windowStart + 1}–${input.windowEnd} / ${MAX_DISPLAYED_SESSIONS} results · ${input.matchCount} matches total`;
567
+ return withDetail(
568
+ `Showing ${input.windowStart + 1}–${input.windowEnd} / ${MAX_DISPLAYED_SESSIONS} results · ${input.matchCount} matches total`,
569
+ );
491
570
  }
492
- return `${input.matchCount} ${input.matchCount === 1 ? "match" : "matches"}`;
571
+ return withDetail(
572
+ `${input.matchCount} ${input.matchCount === 1 ? "match" : "matches"}`,
573
+ );
493
574
  }
494
575
  if (input.totalCount > MAX_DISPLAYED_SESSIONS) {
495
- return `Showing ${input.windowStart + 1}–${input.windowEnd} / ${MAX_DISPLAYED_SESSIONS} recent · ${input.totalCount} sessions total`;
576
+ return withDetail(
577
+ `Showing ${input.windowStart + 1}–${input.windowEnd} / ${MAX_DISPLAYED_SESSIONS} recent · ${input.totalCount} sessions total`,
578
+ );
496
579
  }
497
- return formatWindowStatus(input.windowStart, input.windowEnd, input.totalCount);
580
+ return withDetail(
581
+ formatWindowStatus(input.windowStart, input.windowEnd, input.totalCount),
582
+ );
498
583
  }
499
584
 
500
585
  function formatWindowStatus(start: number, end: number, total: number): string {
@@ -515,7 +600,3 @@ function clamp(value: number, minimum: number, maximum: number): number {
515
600
  function singleLine(value: string): string {
516
601
  return value.replace(/\s+/g, " ").trim();
517
602
  }
518
-
519
- function formatAgo(value: number, unit: "minute" | "hour" | "day"): string {
520
- return `${value} ${unit}${value === 1 ? "" : "s"} ago`;
521
- }
@@ -282,6 +282,33 @@ export function reduceTuiProjection(
282
282
  : "failed",
283
283
  }),
284
284
  );
285
+ case "tool.user_question.requested":
286
+ return updateActiveTurn(state, event, policy, (turn) =>
287
+ updateTurnItem(
288
+ turn,
289
+ toolCallRef(requireEventString(event.toolCallId, "AskUser toolCallId")),
290
+ (item) => ({ ...item, text: `AskUser -> ${event.data.question}` }),
291
+ ),
292
+ );
293
+ case "tool.user_question.resolved":
294
+ return updateActiveTurn(state, event, policy, (turn) =>
295
+ appendTurnItem(turn, {
296
+ id: `answer-${event.toolCallId}-${event.eventSequence}`,
297
+ label: "answer",
298
+ text:
299
+ event.data.outcome === "selected"
300
+ ? event.data.answer
301
+ : event.data.outcome === "dismissed"
302
+ ? "dismissed"
303
+ : "cancelled",
304
+ status:
305
+ event.data.outcome === "selected"
306
+ ? "ok"
307
+ : event.data.outcome === "dismissed"
308
+ ? "info"
309
+ : "cancelled",
310
+ }),
311
+ );
285
312
  case "bash.task.backgrounded":
286
313
  case "bash.task.stopping":
287
314
  case "bash.task.finished":
@@ -800,6 +827,12 @@ function toolCallSummary(input: { name: string; args: unknown }): string {
800
827
  if (input.name === "MemorySearch") {
801
828
  return `MemorySearch ${memorySearchDetail(input.args)}`.trim();
802
829
  }
830
+ if (input.name === "MemoryCreate") {
831
+ return "MemoryCreate";
832
+ }
833
+ if (input.name === "MemoryUpdate" || input.name === "MemoryDelete") {
834
+ return `${input.name} ${memoryMutationId(input.args) ?? ""}`.trim();
835
+ }
803
836
  if (input.name === "WebFetch") {
804
837
  return `WebFetch ${toolUrl(input.args) ?? ""}`.trim();
805
838
  }
@@ -816,6 +849,10 @@ function toolCallSummary(input: { name: string; args: unknown }): string {
816
849
  if (input.name === "Wait") {
817
850
  return `Wait ${toolSeconds(input.args) ?? ""}`.trim();
818
851
  }
852
+ if (input.name === "AskUser") {
853
+ const question = stringProperty(asRecord(input.args), "question");
854
+ return `AskUser${question === undefined ? "" : ` -> ${question}`}`;
855
+ }
819
856
  if (input.name === "Skill") {
820
857
  return `Skill ${stringProperty(asRecord(input.args), "name") ?? ""}`.trim();
821
858
  }
@@ -915,6 +952,12 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
915
952
  return base;
916
953
  }
917
954
  return raw.memory === null ? `${base} -> not found` : `${base} -> found`;
955
+ case "memory_create":
956
+ case "memory_update":
957
+ case "memory_delete":
958
+ return raw.ok
959
+ ? `${name} ${raw.memoryId} -> ${raw.status.replaceAll("_", " ")}`
960
+ : base;
918
961
  case "skill":
919
962
  if (!raw.ok) {
920
963
  return `${base} failed -> ${boundedToolError(raw.error)}`;
@@ -954,6 +997,8 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
954
997
  }
955
998
  case "wait":
956
999
  return raw.ok ? `${base} -> done` : base;
1000
+ case "ask_user":
1001
+ return base;
957
1002
  case "mcp":
958
1003
  case "generic":
959
1004
  return base;
@@ -1005,7 +1050,11 @@ function toolRawResultBashDetail(raw: ToolRawResult): Pick<TimelineItem, "bash">
1005
1050
  case "context_maintenance":
1006
1051
  case "memory_search":
1007
1052
  case "memory_get":
1053
+ case "memory_create":
1054
+ case "memory_update":
1055
+ case "memory_delete":
1008
1056
  case "wait":
1057
+ case "ask_user":
1009
1058
  case "skill":
1010
1059
  case "mcp":
1011
1060
  case "generic":
@@ -1047,7 +1096,11 @@ function toolRawResultDiff(
1047
1096
  case "context_maintenance":
1048
1097
  case "memory_search":
1049
1098
  case "memory_get":
1099
+ case "memory_create":
1100
+ case "memory_update":
1101
+ case "memory_delete":
1050
1102
  case "wait":
1103
+ case "ask_user":
1051
1104
  case "skill":
1052
1105
  case "mcp":
1053
1106
  case "generic":
@@ -1127,6 +1180,10 @@ function memorySearchDetail(args: unknown): string {
1127
1180
  return parts.join(" ");
1128
1181
  }
1129
1182
 
1183
+ function memoryMutationId(args: unknown): string | undefined {
1184
+ return stringProperty(asRecord(args), "id");
1185
+ }
1186
+
1130
1187
  function toolKeywords(args: unknown): string | undefined {
1131
1188
  const limit = 120;
1132
1189
  const value = asRecord(args).keywords;
@@ -5,6 +5,8 @@ import type {
5
5
  import type {
6
6
  ExecuteTurnInput,
7
7
  AcceptedTurn,
8
+ AskUserSnapshot,
9
+ AskUserResolution,
8
10
  BashGuardSnapshot,
9
11
  RuntimeSession,
10
12
  RuntimeSkillsSnapshot,
@@ -52,6 +54,9 @@ export type TuiSessionBinding = {
52
54
  subscribeBashGuard(listener: () => void): () => void;
53
55
  setYoloMode(enabled: boolean): void;
54
56
  resolveBashConfirmation(decision: "allow" | "deny"): Promise<void>;
57
+ askUser(): AskUserSnapshot;
58
+ subscribeAskUser(listener: () => void): () => void;
59
+ resolveAskUser(response: AskUserResolution): Promise<void>;
55
60
  };
56
61
 
57
62
  export type TuiSessionController = {
@@ -262,6 +267,9 @@ export function managedTuiBinding(input: {
262
267
  setYoloMode: (enabled) => input.runtimeSession.setYoloMode(enabled),
263
268
  resolveBashConfirmation: (decision) =>
264
269
  input.runtimeSession.resolveBashConfirmation(decision),
270
+ askUser: () => input.runtimeSession.askUser(),
271
+ subscribeAskUser: (listener) => input.runtimeSession.subscribeAskUser(listener),
272
+ resolveAskUser: (response) => input.runtimeSession.resolveAskUser(response),
265
273
  };
266
274
  }
267
275