parallel-codex-tui 0.2.9 → 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.
@@ -236,6 +236,9 @@ function viewLabel(view) {
236
236
  return shortViewLabel(view);
237
237
  }
238
238
  function shortcutHint(view) {
239
+ if (view === "roles") {
240
+ return "^E back";
241
+ }
239
242
  if (view === "status") {
240
243
  return "^S back";
241
244
  }
@@ -245,6 +248,9 @@ function shortcutHint(view) {
245
248
  return "^S status · ^C exit";
246
249
  }
247
250
  function shortViewLabel(view) {
251
+ if (view === "roles") {
252
+ return "roles";
253
+ }
248
254
  if (view === "status") {
249
255
  return "status";
250
256
  }
@@ -272,7 +278,7 @@ function shortViewLabel(view) {
272
278
  return "chat";
273
279
  }
274
280
  function shortShortcutHint(view) {
275
- return view === "native" ? "^]" : view === "status" ? "^S" : "^C";
281
+ return view === "native" ? "^]" : view === "status" ? "^S" : view === "roles" ? "^E" : "^C";
276
282
  }
277
283
  function compactHeaderProject(cwd, maxLength) {
278
284
  const project = basename(cwd) || cwd;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Box, Text } from "ink";
3
3
  import { compactEndByDisplayWidth, compactTailByDisplayWidth, displayWidth } from "./display-width.js";
4
4
  import { TUI_THEME } from "./theme.js";
5
- export function InputBar({ mode, ready = true, busy = false, routeFallback = false, collaborationDetail = false, collaborationUnresolved = false, collaborationBack = "workers", featureCanCancel = false, featureCanPause = false, featureCanReassign = false, featureCancelConfirm = false, featurePauseConfirm = false, featureAssignment = false, taskSessionAction = null, taskSessionsIncludeArchived = false, mainConversationSessions = false, taskSessionDetail = false, taskSessionDetailHasNative = false, taskSessionDetailCanFork = false, canRetry = false, hasWorkers = false, hasActiveTask = false, hasTaskResult = false, taskResultExpanded = false, chatScrollOffset = 0, chatMaxScrollOffset = 0, nativeClosed = false, searchMatchIndex = 0, searchMatchCount = 0, clipboardNotice = null, value, cursor, terminalWidth: providedTerminalWidth, onChange, onSubmit }) {
5
+ export function InputBar({ mode, ready = true, busy = false, routeFallback = false, collaborationDetail = false, collaborationUnresolved = false, collaborationBack = "workers", featureCanCancel = false, featureCanPause = false, featureCanReassign = false, featureCancelConfirm = false, featurePauseConfirm = false, featureAssignment = false, taskSessionAction = null, taskSessionsIncludeArchived = false, mainConversationSessions = false, taskSessionDetail = false, taskSessionDetailHasNative = false, taskSessionDetailCanFork = false, canRetry = false, hasWorkers = false, hasActiveTask = false, hasTaskResult = false, taskResultExpanded = false, chatScrollOffset = 0, chatMaxScrollOffset = 0, nativeClosed = false, searchMatchIndex = 0, searchMatchCount = 0, clipboardNotice = null, roleScope = "next", roleEditingModel = null, roleCanApply = true, roleSaving = false, roleHasOverride = false, value, cursor, terminalWidth: providedTerminalWidth, onChange, onSubmit }) {
6
6
  const terminalWidth = providedTerminalWidth ?? process.stdout.columns ?? 120;
7
7
  const fillRail = providedTerminalWidth !== undefined || typeof process.stdout.columns === "number";
8
8
  if (clipboardNotice) {
@@ -19,6 +19,17 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
19
19
  const textWidth = displayWidth(`${prefix}${display.before}|${display.after}${suffix}`);
20
20
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: textWidth, fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: prefix }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.text, children: display.before }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: "|" }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.text, children: display.after }), suffix ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: suffix }) : null] }));
21
21
  }
22
+ if (mode === "roles") {
23
+ if (roleEditingModel) {
24
+ const prefix = `${roleEditingModel.role} model > `;
25
+ const valueWidth = Math.max(1, terminalWidth - displayWidth(prefix) - 3);
26
+ const display = chatInputDisplayParts(roleEditingModel.value, roleEditingModel.cursor, valueWidth);
27
+ const textWidth = displayWidth(`${prefix}${display.before}|${display.after}`);
28
+ return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: textWidth, fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: prefix }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.text, children: display.before }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: "|" }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.text, children: display.after })] }));
29
+ }
30
+ const hints = roleConfigurationInputHints(terminalWidth, roleScope, roleCanApply, roleSaving, roleHasOverride);
31
+ return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: roleSaving ? TUI_THEME.warning : TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
32
+ }
22
33
  if (mode === "status") {
23
34
  const hints = statusDetailInputHints(terminalWidth);
24
35
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
@@ -40,7 +51,7 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
40
51
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
41
52
  }
42
53
  if (mainConversationSessions) {
43
- const hints = mainConversationSessionsInputHints(terminalWidth);
54
+ const hints = mainConversationSessionsInputHints(terminalWidth, taskSessionsIncludeArchived);
44
55
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
45
56
  }
46
57
  const hints = taskSessionsInputHints(terminalWidth, taskSessionsIncludeArchived);
@@ -422,14 +433,27 @@ function workerInputHints(width) {
422
433
  }
423
434
  function statusDetailInputHints(width) {
424
435
  return selectInputHints(width, [
425
- { label: "status", detail: " · ^X diagnostics · ^S/Esc back · ^C exit" },
426
- { label: "status", detail: " · ^X diag · ^S/Esc back · ^C exit" },
436
+ { label: "status", detail: " · ^E roles · ^X diagnostics · ^S/Esc back · ^C exit" },
437
+ { label: "status", detail: " · ^E roles · ^X diag · ^S/Esc back · ^C exit" },
427
438
  { label: "status", detail: " · ^S back · ^C exit" },
428
439
  { label: "status", detail: " · ^S back" },
429
440
  { label: "status", detail: "" },
430
441
  { label: "st", detail: "" }
431
442
  ]);
432
443
  }
444
+ function roleConfigurationInputHints(width, scope, canApply, saving, hasOverride) {
445
+ const label = saving ? "roles · saving" : `roles · ${scope}`;
446
+ const apply = canApply ? "Enter apply" : "task unavailable";
447
+ const reset = hasOverride ? " · X reset" : "";
448
+ return selectInputHints(width, [
449
+ { label, detail: ` · Tab scope · Up/Dn role · Left/Right provider · M model · ${apply}${reset} · ^E/Esc back` },
450
+ { label, detail: ` · Tab scope · Up/Dn · Left/Right provider · M model · ${apply}${reset} · Esc back` },
451
+ { label, detail: ` · Tab · Up/Dn · Left/Right · M model · ${apply}${reset}` },
452
+ { label, detail: ` · Tab · arrows · M · ${canApply ? "Enter" : "no task"}` },
453
+ { label: "roles", detail: " · Tab · arrows · M · Enter" },
454
+ { label: "roles", detail: "" }
455
+ ]);
456
+ }
433
457
  function workerOverviewInputHints(width) {
434
458
  if (width < 16) {
435
459
  return { label: "wrk", detail: "" };
@@ -588,8 +612,14 @@ function taskSessionsInputHints(width, includeArchived) {
588
612
  { label: "s", detail: "" }
589
613
  ]);
590
614
  }
591
- function mainConversationSessionsInputHints(width) {
615
+ export function mainConversationSessionsInputHints(width, includeArchived) {
616
+ const archivedAction = includeArchived ? "H hide archived" : "H archived";
592
617
  return selectInputHints(width, [
618
+ { label: "conversations", detail: ` · Up/Dn select · Enter restore · R rename · A archive · D delete · E export · ${archivedAction} · N new · T tasks · Esc back` },
619
+ { label: "conversations", detail: " · Up/Dn select · Enter restore · R rename · A archive · D delete · E export · N new · T tasks · Esc back" },
620
+ { label: "conversations", detail: " · Up/Dn select · Enter restore · R rename · A archive · D delete · N new · T tasks · Esc back" },
621
+ { label: "conversations", detail: " · Up/Dn select · Enter restore · R rename · A archive · N new · T tasks · Esc back" },
622
+ { label: "conversations", detail: " · Up/Dn select · Enter restore · R rename · N new · T tasks · Esc back" },
593
623
  { label: "conversations", detail: " · Up/Dn select · Enter restore · N new · T tasks · Esc back" },
594
624
  { label: "conversations", detail: " · Up/Dn select · Enter restore · T tasks · Esc back" },
595
625
  { label: "conversations", detail: " · Up/Dn select · Enter restore · Esc back" },
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { compactEndByDisplayWidth, displayWidth } from "./display-width.js";
4
4
  import { TUI_THEME } from "./theme.js";
5
- export function MainConversationsView({ conversations, selectedIndex, notice = null, loading = false, error = null, height = 20, terminalWidth = process.stdout.columns || 120 }) {
5
+ export function MainConversationsView({ conversations, selectedIndex, includeArchived = false, notice = null, action = null, loading = false, error = null, height = 20, terminalWidth = process.stdout.columns || 120 }) {
6
6
  const viewportHeight = Math.max(1, height);
7
7
  const width = mainConversationsContentWidth(terminalWidth);
8
- const lines = mainConversationsDisplayLines(conversations, selectedIndex, viewportHeight, terminalWidth, { notice, loading, error });
8
+ const lines = mainConversationsDisplayLines(conversations, selectedIndex, viewportHeight, terminalWidth, { includeArchived, notice, action, loading, error });
9
9
  const blankRows = Math.max(0, viewportHeight - lines.length);
10
10
  return (_jsxs(Box, { flexDirection: "column", height: viewportHeight, children: [lines.map((line, index) => (_jsx(MainConversationRow, { line: line, width: width }, `${line.conversationIndex ?? line.tone}-${index}`))), Array.from({ length: blankRows }, (_, index) => (_jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(width) }, `main-conversation-fill-${index}`)))] }));
11
11
  }
@@ -13,15 +13,26 @@ export function mainConversationsDisplayLines(conversations, selectedIndex, heig
13
13
  const viewportHeight = Math.max(1, Math.trunc(height));
14
14
  const width = mainConversationsContentWidth(terminalWidth);
15
15
  const lines = [{
16
- text: fitMainConversationCandidates(["Main conversations", "Conversations", "Chats", "C"], width),
16
+ text: fitMainConversationCandidates([
17
+ state.includeArchived ? "Main conversations · archived shown" : "Main conversations",
18
+ state.includeArchived ? "Conversations · all" : "Conversations",
19
+ "Chats",
20
+ "C"
21
+ ], width),
17
22
  tone: "heading"
18
23
  }];
19
24
  if (viewportHeight >= 3) {
20
25
  const messages = conversations.reduce((sum, conversation) => sum + conversation.messageCount, 0);
21
26
  const nativeSessions = conversations.reduce((sum, conversation) => sum + conversation.nativeSessionCount, 0);
27
+ const archived = conversations.filter((conversation) => conversation.archivedAt).length;
22
28
  lines.push({
23
29
  text: fitMainConversationCandidates([
24
- `${conversations.length} ${conversations.length === 1 ? "conversation" : "conversations"} · ${messages} messages · ${nativeSessions} native`,
30
+ [
31
+ `${conversations.length} ${conversations.length === 1 ? "conversation" : "conversations"}`,
32
+ `${messages} messages`,
33
+ `${nativeSessions} native`,
34
+ ...(archived > 0 ? [`${archived} archived`] : [])
35
+ ].join(" · "),
25
36
  `${conversations.length} conversations · ${messages} messages`,
26
37
  `${conversations.length} conversations`,
27
38
  `${conversations.length} chats`
@@ -29,7 +40,15 @@ export function mainConversationsDisplayLines(conversations, selectedIndex, heig
29
40
  tone: "muted"
30
41
  });
31
42
  }
32
- if (viewportHeight >= 4 && state.notice) {
43
+ if (viewportHeight >= 4 && state.action) {
44
+ lines.push({
45
+ text: fitMainConversationText(state.action.type === "rename"
46
+ ? `rename · ${safeMainConversationText(state.action.title)} · Enter save · Esc cancel`
47
+ : `delete · ${safeMainConversationText(state.action.title)} · press D again · Esc cancel`, width),
48
+ tone: state.action.type === "delete" ? "danger" : "active"
49
+ });
50
+ }
51
+ else if (viewportHeight >= 4 && state.notice) {
33
52
  lines.push({ text: fitMainConversationText(state.notice, width), tone: "success" });
34
53
  }
35
54
  const slots = Math.max(0, viewportHeight - lines.length);
@@ -88,14 +107,15 @@ function mainConversationRowText(conversation, selected, width) {
88
107
  const title = safeMainConversationText(conversation.title);
89
108
  const messages = `${conversation.messageCount} ${conversation.messageCount === 1 ? "message" : "messages"}`;
90
109
  const native = `${conversation.nativeSessionCount} native`;
110
+ const status = conversation.archivedAt ? "archived" : null;
91
111
  const date = conversation.lastActivityAt.slice(5, 16).replace("T", " ");
92
112
  const scope = conversation.id
93
113
  ? `#${conversation.id.replace(/^conversation-/, "")}`
94
114
  : "legacy";
95
115
  return fitMainConversationCandidates([
96
- [marker + title, messages, native, date].join(" · "),
97
- [marker + title, messages, native].join(" · "),
98
- [marker + title, messages].join(" · "),
116
+ [marker + title, status, messages, native, date].filter(Boolean).join(" · "),
117
+ [marker + title, status, messages, native].filter(Boolean).join(" · "),
118
+ [marker + title, status, messages].filter(Boolean).join(" · "),
99
119
  [marker + title, date].join(" · "),
100
120
  [marker + scope, messages].join(" · "),
101
121
  marker.trimEnd()
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { CONFIGURABLE_ROLES } from "../core/role-configuration.js";
4
+ import { compactEndByDisplayWidth, displayWidth } from "./display-width.js";
5
+ import { roleConfigurationScopeHasOverride, roleConfigurationScopeLabel } from "./role-configuration-state.js";
6
+ import { TUI_THEME } from "./theme.js";
7
+ export function RoleConfigurationView({ height = 20, terminalWidth = process.stdout.columns || 120, ...input }) {
8
+ const viewportHeight = Math.max(1, Math.trunc(height));
9
+ const width = Math.max(1, terminalWidth - 2);
10
+ const lines = roleConfigurationDisplayLines(input, width).slice(0, viewportHeight);
11
+ const blanks = Math.max(0, viewportHeight - lines.length);
12
+ return (_jsxs(Box, { flexDirection: "column", height: viewportHeight, children: [lines.map((line, index) => (_jsx(RoleConfigurationRow, { line: line, width: width }, `${line.text}-${index}`))), Array.from({ length: blanks }, (_, index) => (_jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(width) }, `role-config-fill-${index}`)))] }));
13
+ }
14
+ export function roleConfigurationDisplayLines(input, width) {
15
+ const safeWidth = Math.max(1, Math.trunc(width));
16
+ const lines = [
17
+ { text: "Role & model control", tone: "heading" },
18
+ {
19
+ text: `scope · ${roleConfigurationScopeLabel(input.scope)}`,
20
+ tone: input.scope === "task" && !input.hasActiveTask ? "warning" : "accent"
21
+ }
22
+ ];
23
+ if (input.loading || !input.snapshot || !input.draft) {
24
+ lines.push({ text: "loading role configuration...", tone: "muted" });
25
+ return lines.map((line) => fittedRoleLine(line, safeWidth));
26
+ }
27
+ if (input.scope === "task" && !input.hasActiveTask) {
28
+ lines.push({ text: "No active Task · choose next request or future requests.", tone: "warning" });
29
+ }
30
+ else {
31
+ lines.push({
32
+ text: roleConfigurationScopeHasOverride(input.snapshot, input.scope)
33
+ ? "saved override · Enter updates it · X resets inheritance"
34
+ : "inheriting defaults · Enter saves this matrix",
35
+ tone: roleConfigurationScopeHasOverride(input.snapshot, input.scope) ? "success" : "muted"
36
+ });
37
+ }
38
+ lines.push({ text: "", tone: "text" });
39
+ for (const [index, role] of CONFIGURABLE_ROLES.entries()) {
40
+ const target = input.draft[role];
41
+ const provider = input.snapshot.providers.find((candidate) => candidate.id === target.engine);
42
+ const remote = provider?.modelProvider.trim();
43
+ const model = target.model.trim() || provider?.model.trim() || "default";
44
+ const prefix = index === input.selectedRoleIndex ? ">" : " ";
45
+ const roleLabel = `${role[0]?.toUpperCase() ?? ""}${role.slice(1)}`.padEnd(7, " ");
46
+ lines.push({
47
+ text: `${prefix} ${roleLabel} ${target.engine} · ${[remote, model].filter(Boolean).join("/")}`,
48
+ tone: index === input.selectedRoleIndex ? "accent" : "text",
49
+ selected: index === input.selectedRoleIndex
50
+ });
51
+ }
52
+ lines.push({ text: "", tone: "text" });
53
+ const active = input.snapshot.activeTurn;
54
+ if (active) {
55
+ lines.push({
56
+ text: `active turn · ${CONFIGURABLE_ROLES.map((role) => `${role}/${active[role].engine}/${active[role].model || "default"}`).join(" · ")}`,
57
+ tone: "muted"
58
+ });
59
+ }
60
+ if (input.saving) {
61
+ lines.push({ text: "saving role configuration...", tone: "warning" });
62
+ }
63
+ else if (input.error?.trim()) {
64
+ lines.push({ text: `error · ${input.error.trim()}`, tone: "danger" });
65
+ }
66
+ else if (input.notice?.trim()) {
67
+ lines.push({ text: input.notice.trim(), tone: "success" });
68
+ }
69
+ return lines.map((line) => fittedRoleLine(line, safeWidth));
70
+ }
71
+ function RoleConfigurationRow({ line, width }) {
72
+ const text = compactEndByDisplayWidth(line.text, width);
73
+ const trailing = Math.max(0, width - displayWidth(text));
74
+ const theme = roleConfigurationLineTheme(line.tone, line.selected);
75
+ return (_jsxs(Text, { children: [_jsx(Text, { ...theme, children: text }), trailing > 0 ? _jsx(Text, { backgroundColor: theme.backgroundColor, children: " ".repeat(trailing) }) : null] }));
76
+ }
77
+ export function roleConfigurationLineTheme(tone, selected = false) {
78
+ return {
79
+ backgroundColor: selected ? TUI_THEME.rail : TUI_THEME.surface,
80
+ color: tone === "heading" || tone === "accent"
81
+ ? TUI_THEME.accent
82
+ : tone === "success"
83
+ ? TUI_THEME.success
84
+ : tone === "warning"
85
+ ? TUI_THEME.warning
86
+ : tone === "danger"
87
+ ? TUI_THEME.danger
88
+ : tone === "muted"
89
+ ? TUI_THEME.muted
90
+ : TUI_THEME.text,
91
+ ...(tone === "heading" || tone === "danger" || selected ? { bold: true } : {})
92
+ };
93
+ }
94
+ function fittedRoleLine(line, width) {
95
+ return { ...line, text: compactEndByDisplayWidth(line.text, width) };
96
+ }
@@ -25,7 +25,7 @@ export function statusDetailDisplayLines(input, width, height) {
25
25
  tone: "muted"
26
26
  },
27
27
  {
28
- text: fitStatusDetailText(rolePairingDetail(input.pairing), safeWidth),
28
+ text: fitStatusDetailText(rolePairingDetail(input.pairing, input.roleSelection), safeWidth),
29
29
  tone: "text"
30
30
  }
31
31
  ];
@@ -59,15 +59,18 @@ export function statusDetailDisplayLines(input, width, height) {
59
59
  }
60
60
  return lines.slice(0, maxLines);
61
61
  }
62
- function rolePairingDetail(pairing) {
62
+ function rolePairingDetail(pairing, selection) {
63
63
  return [
64
64
  "active roles",
65
- `main/${pairing.main}`,
66
- `judge/${pairing.judge}`,
67
- `actor/${pairing.actor}`,
68
- `critic/${pairing.critic}`
65
+ rolePairingEntry("main", selection?.main.engine ?? pairing.main, selection?.main.model),
66
+ rolePairingEntry("judge", selection?.judge.engine ?? pairing.judge, selection?.judge.model),
67
+ rolePairingEntry("actor", selection?.actor.engine ?? pairing.actor, selection?.actor.model),
68
+ rolePairingEntry("critic", selection?.critic.engine ?? pairing.critic, selection?.critic.model)
69
69
  ].join(" · ");
70
70
  }
71
+ function rolePairingEntry(role, engine, model) {
72
+ return [role, engine, model?.trim()].filter(Boolean).join("/");
73
+ }
71
74
  function taskDetail(input) {
72
75
  if (!input.taskId) {
73
76
  return "task · none";
@@ -38,6 +38,9 @@ export function isCopyShortcut(input, key) {
38
38
  export function isDiagnosticsShortcut(input, key) {
39
39
  return (key.ctrl === true && input.toLowerCase() === "x") || input === "\u0018";
40
40
  }
41
+ export function isRoleConfigurationShortcut(input, key) {
42
+ return (key.ctrl === true && input.toLowerCase() === "e") || input === "\u0005";
43
+ }
41
44
  export function workerLogJumpKind(input) {
42
45
  if (input === "e" || input === "E") {
43
46
  return "error";
@@ -80,6 +83,14 @@ export function rawPlainArrowDelta(input) {
80
83
  }
81
84
  return delta;
82
85
  }
86
+ export function rawHorizontalArrowDelta(input) {
87
+ let delta = 0;
88
+ for (const match of input.matchAll(/\x1b(?:O([CD])|\[[0-9;?]*([CD]))/g)) {
89
+ const direction = match[1] ?? match[2];
90
+ delta += direction === "C" ? 1 : -1;
91
+ }
92
+ return delta;
93
+ }
83
94
  export function rawChatScrollArrowDelta(input) {
84
95
  const delta = rawPlainArrowDelta(input);
85
96
  // Alternate-scroll wheels arrive as arrow bursts; one arrow remains a draft-history keypress.
@@ -0,0 +1,74 @@
1
+ import { CONFIGURABLE_ROLES, cloneRoleSelection } from "../core/role-configuration.js";
2
+ const ROLE_CONFIGURATION_SCOPES = ["next", "task", "future"];
3
+ export function roleConfigurationSelectionForScope(snapshot, scope) {
4
+ if (scope === "future") {
5
+ return cloneRoleSelection(snapshot.future);
6
+ }
7
+ if (scope === "task") {
8
+ return cloneRoleSelection(snapshot.task ?? snapshot.future);
9
+ }
10
+ return cloneRoleSelection(snapshot.next ?? snapshot.future);
11
+ }
12
+ export function roleConfigurationScopeHasOverride(snapshot, scope) {
13
+ if (!snapshot) {
14
+ return false;
15
+ }
16
+ if (scope === "future") {
17
+ return !sameRoleSelection(snapshot.future, snapshot.baseline);
18
+ }
19
+ return scope === "next" ? snapshot.next !== null : snapshot.task !== null;
20
+ }
21
+ export function nextRoleConfigurationScope(scope, delta, hasTask) {
22
+ const available = ROLE_CONFIGURATION_SCOPES.filter((candidate) => candidate !== "task" || hasTask);
23
+ const index = Math.max(0, available.indexOf(scope));
24
+ return available[((index + delta) % available.length + available.length) % available.length] ?? "next";
25
+ }
26
+ export function moveRoleConfigurationSelection(index, delta) {
27
+ const length = CONFIGURABLE_ROLES.length;
28
+ return ((index + delta) % length + length) % length;
29
+ }
30
+ export function selectedConfigurableRole(index) {
31
+ return CONFIGURABLE_ROLES[Math.max(0, Math.min(CONFIGURABLE_ROLES.length - 1, index))] ?? "main";
32
+ }
33
+ export function cycleRoleProvider(roles, role, providers, delta) {
34
+ const currentEngine = roles[role].engine;
35
+ const selectableProviders = providers.filter((provider) => (provider.assignable || provider.id === currentEngine));
36
+ if (selectableProviders.length === 0) {
37
+ return cloneRoleSelection(roles);
38
+ }
39
+ const currentIndex = selectableProviders.findIndex((provider) => provider.id === currentEngine);
40
+ const start = currentIndex >= 0 ? currentIndex : 0;
41
+ const provider = selectableProviders[((start + delta) % selectableProviders.length + selectableProviders.length) % selectableProviders.length] ?? selectableProviders[0];
42
+ if (!provider) {
43
+ return cloneRoleSelection(roles);
44
+ }
45
+ return {
46
+ ...cloneRoleSelection(roles),
47
+ [role]: {
48
+ engine: provider.id,
49
+ model: provider.model
50
+ }
51
+ };
52
+ }
53
+ export function updateRoleModel(roles, role, model) {
54
+ return {
55
+ ...cloneRoleSelection(roles),
56
+ [role]: {
57
+ ...roles[role],
58
+ model
59
+ }
60
+ };
61
+ }
62
+ export function sameRoleSelection(left, right) {
63
+ return CONFIGURABLE_ROLES.every((role) => (left[role].engine === right[role].engine
64
+ && left[role].model === right[role].model));
65
+ }
66
+ export function roleConfigurationScopeLabel(scope) {
67
+ if (scope === "next") {
68
+ return "next request · one shot";
69
+ }
70
+ if (scope === "task") {
71
+ return "current task · retries and follow-ups";
72
+ }
73
+ return "future requests · persisted default";
74
+ }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "0.2.9";
1
+ export const version = "0.3.0";
@@ -18,7 +18,11 @@ export async function buildNativeForkLaunch(input) {
18
18
  async function buildNativeSessionLaunch(input, mode) {
19
19
  const workerConfig = workerProvider(input.config, input.worker.engine).config;
20
20
  const nativeSession = await readWorkerNativeSession(input.worker, workerConfig.capabilities.profile);
21
- const modelConfig = workerConfig.model;
21
+ const modelConfig = {
22
+ ...workerConfig.model,
23
+ name: input.worker.runtimeStatus?.model_name?.trim() || workerConfig.model.name,
24
+ provider: input.worker.runtimeStatus?.model_provider?.trim() || workerConfig.model.provider
25
+ };
22
26
  const env = modelEnvironment(modelConfig);
23
27
  const interactiveArgs = mode === "fork"
24
28
  ? workerConfig.interactive.forkArgs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parallel-codex-tui",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "A TypeScript TUI wrapper for routed parallel coding with Codex and Claude workers.",
5
5
  "license": "MIT",
6
6
  "type": "module",