@a5c-ai/agent-mux-tui 0.4.1 → 0.4.10-staging.89ec25ae

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 (129) hide show
  1. package/README.md +69 -9
  2. package/dist/app.d.ts +21 -3
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +249 -77
  5. package/dist/app.js.map +1 -1
  6. package/dist/bin/amux-tui.d.ts +0 -1
  7. package/dist/bin/amux-tui.js +68 -6
  8. package/dist/bin/amux-tui.js.map +1 -1
  9. package/dist/bin/runtime.d.ts +31 -0
  10. package/dist/bin/runtime.d.ts.map +1 -0
  11. package/dist/bin/runtime.js +78 -0
  12. package/dist/bin/runtime.js.map +1 -0
  13. package/dist/command-palette.d.ts +3 -2
  14. package/dist/command-palette.d.ts.map +1 -1
  15. package/dist/command-palette.js +14 -7
  16. package/dist/command-palette.js.map +1 -1
  17. package/dist/event-stream.d.ts +0 -1
  18. package/dist/external-plugins.d.ts +0 -1
  19. package/dist/external-plugins.d.ts.map +1 -1
  20. package/dist/external-plugins.js +5 -1
  21. package/dist/external-plugins.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/kanban-control-plane.d.ts +44 -0
  27. package/dist/kanban-control-plane.d.ts.map +1 -0
  28. package/dist/kanban-control-plane.js +30 -0
  29. package/dist/kanban-control-plane.js.map +1 -0
  30. package/dist/layout.d.ts +24 -0
  31. package/dist/layout.d.ts.map +1 -0
  32. package/dist/layout.js +64 -0
  33. package/dist/layout.js.map +1 -0
  34. package/dist/model-picker.d.ts +4 -2
  35. package/dist/model-picker.d.ts.map +1 -1
  36. package/dist/model-picker.js +9 -5
  37. package/dist/model-picker.js.map +1 -1
  38. package/dist/plugin.d.ts +26 -1
  39. package/dist/plugin.d.ts.map +1 -1
  40. package/dist/plugin.js.map +1 -1
  41. package/dist/plugins/adapters-view.d.ts +0 -1
  42. package/dist/plugins/agents-view.d.ts +0 -1
  43. package/dist/plugins/agents-view.d.ts.map +1 -1
  44. package/dist/plugins/agents-view.js +59 -15
  45. package/dist/plugins/agents-view.js.map +1 -1
  46. package/dist/plugins/approval.d.ts +0 -1
  47. package/dist/plugins/auth-view.d.ts +0 -1
  48. package/dist/plugins/chat-view.d.ts +0 -1
  49. package/dist/plugins/config-view.d.ts +0 -1
  50. package/dist/plugins/control.d.ts +0 -1
  51. package/dist/plugins/cost-alerts.d.ts +0 -1
  52. package/dist/plugins/cost-view.d.ts +0 -1
  53. package/dist/plugins/cost.d.ts +0 -1
  54. package/dist/plugins/diff.d.ts +0 -1
  55. package/dist/plugins/diff.js.map +1 -1
  56. package/dist/plugins/doctor-view.d.ts +0 -1
  57. package/dist/plugins/fallback.d.ts +0 -1
  58. package/dist/plugins/file-ops.d.ts +0 -1
  59. package/dist/plugins/help-view.d.ts +0 -1
  60. package/dist/plugins/help-view.d.ts.map +1 -1
  61. package/dist/plugins/help-view.js +5 -3
  62. package/dist/plugins/help-view.js.map +1 -1
  63. package/dist/plugins/hooks-view.d.ts +0 -1
  64. package/dist/plugins/hooks-view.js.map +1 -1
  65. package/dist/plugins/image.d.ts +0 -1
  66. package/dist/plugins/kanban-view.d.ts +4 -0
  67. package/dist/plugins/kanban-view.d.ts.map +1 -0
  68. package/dist/plugins/kanban-view.js +480 -0
  69. package/dist/plugins/kanban-view.js.map +1 -0
  70. package/dist/plugins/lifecycle.d.ts +0 -1
  71. package/dist/plugins/mcp-view.d.ts +0 -1
  72. package/dist/plugins/mcp-view.d.ts.map +1 -1
  73. package/dist/plugins/mcp-view.js +10 -7
  74. package/dist/plugins/mcp-view.js.map +1 -1
  75. package/dist/plugins/mcp.d.ts +0 -1
  76. package/dist/plugins/models-view.d.ts +0 -1
  77. package/dist/plugins/models-view.d.ts.map +1 -1
  78. package/dist/plugins/models-view.js +10 -4
  79. package/dist/plugins/models-view.js.map +1 -1
  80. package/dist/plugins/observability-view.d.ts +20 -1
  81. package/dist/plugins/observability-view.d.ts.map +1 -1
  82. package/dist/plugins/observability-view.js +62 -48
  83. package/dist/plugins/observability-view.js.map +1 -1
  84. package/dist/plugins/plugin-skill.d.ts +0 -1
  85. package/dist/plugins/plugins-view.d.ts +0 -1
  86. package/dist/plugins/plugins-view.d.ts.map +1 -1
  87. package/dist/plugins/plugins-view.js +2 -14
  88. package/dist/plugins/plugins-view.js.map +1 -1
  89. package/dist/plugins/profiles-view.d.ts +0 -1
  90. package/dist/plugins/session-detail-view.d.ts +1 -2
  91. package/dist/plugins/session-detail-view.d.ts.map +1 -1
  92. package/dist/plugins/session-detail-view.js +19 -14
  93. package/dist/plugins/session-detail-view.js.map +1 -1
  94. package/dist/plugins/session-lifecycle.d.ts +0 -1
  95. package/dist/plugins/sessions-view.d.ts +0 -1
  96. package/dist/plugins/sessions-view.d.ts.map +1 -1
  97. package/dist/plugins/sessions-view.js +25 -11
  98. package/dist/plugins/sessions-view.js.map +1 -1
  99. package/dist/plugins/shell.d.ts +0 -1
  100. package/dist/plugins/skills-view.d.ts +0 -1
  101. package/dist/plugins/skills-view.d.ts.map +1 -1
  102. package/dist/plugins/skills-view.js +54 -9
  103. package/dist/plugins/skills-view.js.map +1 -1
  104. package/dist/plugins/subagent.d.ts +0 -1
  105. package/dist/plugins/text-delta.d.ts +0 -1
  106. package/dist/plugins/tool-call.d.ts +0 -1
  107. package/dist/plugins/tool-call.d.ts.map +1 -1
  108. package/dist/plugins/tool-call.js +89 -17
  109. package/dist/plugins/tool-call.js.map +1 -1
  110. package/dist/plugins/workspaces-view.d.ts +4 -0
  111. package/dist/plugins/workspaces-view.d.ts.map +1 -0
  112. package/dist/plugins/workspaces-view.js +382 -0
  113. package/dist/plugins/workspaces-view.js.map +1 -0
  114. package/dist/prompt-history-store.d.ts +0 -1
  115. package/dist/prompt-input.d.ts +27 -2
  116. package/dist/prompt-input.d.ts.map +1 -1
  117. package/dist/prompt-input.js +195 -12
  118. package/dist/prompt-input.js.map +1 -1
  119. package/dist/registry.d.ts +2 -2
  120. package/dist/registry.d.ts.map +1 -1
  121. package/dist/registry.js +2 -1
  122. package/dist/registry.js.map +1 -1
  123. package/dist/session-dispatch.d.ts +35 -0
  124. package/dist/session-dispatch.d.ts.map +1 -0
  125. package/dist/session-dispatch.js +129 -0
  126. package/dist/session-dispatch.js.map +1 -0
  127. package/package.json +15 -10
  128. package/specs/kanban-workspaces-spec.md +293 -0
  129. package/specs/kanban-workspaces-subtasks.md +180 -0
package/README.md CHANGED
@@ -12,6 +12,38 @@ npm i -g @a5c-ai/agent-mux-tui
12
12
  amux-tui
13
13
  ```
14
14
 
15
+ ## Development
16
+
17
+ Package-level `build` and `test` scripts are routed through the repo-root
18
+ `scripts/agent-mux-build.cjs` helper so `@a5c-ai/agent-mux-tui` runs with its
19
+ agent-mux prerequisites built in dependency order and its tests executed from
20
+ the repo root. Use `npm run build:local --workspace=@a5c-ai/agent-mux-tui`
21
+ when you only want the package-local TypeScript compile.
22
+
23
+ Run `npm run verify:release --workspace=@a5c-ai/agent-mux-tui` after a build to
24
+ confirm the packed package surface still includes the README-linked kanban/workspaces
25
+ planning artifacts.
26
+
27
+ ## Kanban/workspaces surface
28
+
29
+ The package now ships first-phase `kanban` and `workspaces` views backed by the
30
+ shared kanban/workspace control plane.
31
+
32
+ - Runtime today: `kanban` view (list-first issue browsing, issue detail,
33
+ keyboard-driven backlog actions through an injected control plane, and linked
34
+ workspace/session handoff)
35
+ - Runtime today: `workspaces` view (workspace inventory, linked issue/session/run
36
+ context, lifecycle actions routed through the injected control plane, and
37
+ issue/session handoff back into the rest of the TUI)
38
+
39
+ - Spec: [`specs/kanban-workspaces-spec.md`](specs/kanban-workspaces-spec.md)
40
+ - Backlog decomposition: [`specs/kanban-workspaces-subtasks.md`](specs/kanban-workspaces-subtasks.md)
41
+ - Cross-package design note:
42
+ [`../../docs/agent-mux/archive/design/20-tui-kanban-workspaces.md`](../../docs/agent-mux/archive/design/20-tui-kanban-workspaces.md)
43
+
44
+ The planning documents remain useful design references for follow-on expansion
45
+ of both surfaces.
46
+
15
47
  ## Writing a plugin
16
48
 
17
49
  ```ts
@@ -70,6 +102,12 @@ The built-in plugins (`text-delta`, `thinking-delta`, `tool-call`,
70
102
  `tool-error`, `cost`, `chat-view`, `sessions-view`, `fallback`) are all
71
103
  implemented through these same extension points — use them as references.
72
104
 
105
+ The logs / observability view is part of the supported built-in surface. Press
106
+ `l` to open it. The metrics header aggregates the full buffered `EventStream`,
107
+ the global `/` filter only narrows the visible log rows, and pressing `e`
108
+ exports the full buffered stream to `session-log-<timestamp>.json` in the
109
+ current working directory.
110
+
73
111
  ## View hotkeys
74
112
 
75
113
  | Key | View | Purpose |
@@ -81,15 +119,37 @@ implemented through these same extension points — use them as references.
81
119
  | `5` | models | model registry per adapter |
82
120
  | `6` | profiles | run-options profiles |
83
121
  | `7` | plugins | native plugins per adapter |
84
- | `8` | runs | active/recent runs |
122
+ | `8` | kanban | backlog/board browsing + issue actions |
123
+ | `W` | workspaces | workspace lifecycle inventory + actions |
85
124
  | `9` | help | keybindings + tips |
86
125
  | `0` | mcp | registered MCP servers |
87
126
  | `-` | doctor | capability matrix / diagnostics |
88
- | `a` | auth | auth status per adapter |
89
- | `c` | config | config view |
90
- | `k` | skills | installed skills (d: delete, r: refresh) |
91
- | `g` | agents | installed sub-agents (d: delete, r: refresh) |
92
- | `h` | hooks | registered hooks (d: remove, r: refresh) |
93
-
94
- Global: `p` prompt, `/` filter, `:` / Ctrl-K palette, `m` model picker,
95
- `P` profile picker, `i` interrupt, `y`/`n` approval, `q` quit.
127
+ | `l` | logs | observability metrics + filtered log stream |
128
+ | `A` | auth | auth status per adapter |
129
+ | `C` | config | config view |
130
+ | `K` | skills | installed skills (d: delete, r: refresh) |
131
+ | `G` | agents | installed sub-agents (d: delete, r: refresh) |
132
+ | `H` | hooks | registered hooks (d: remove, r: refresh) |
133
+
134
+ Global: chat composer auto-focuses in `chat`; `Esc` temporarily dismisses it;
135
+ `/` filter, `:` / Ctrl-K palette, `m` model picker, `N` agent picker, `P`
136
+ profile picker, `i` interrupt, `y`/`n` approval, `q` quit.
137
+
138
+ Inside `kanban`, press `w` to jump to the linked workspace for the selected
139
+ issue. Inside `workspaces`, press `g` to jump back to the linked issue. If you
140
+ open `session-detail` from either view, `b`/`Esc` returns to the originating
141
+ surface instead of always falling back to `sessions`.
142
+
143
+ ## Logs / Observability View
144
+
145
+ The `logs` view is a built-in tab registered as `builtin:observability-view`.
146
+
147
+ - Hotkey: `l`
148
+ - Purpose: show aggregated tokens, cost, latency, error count, and tool-call
149
+ count for the buffered event stream.
150
+ - Filtering: the global `/` filter applies to the log rows in this view,
151
+ including `type:<prefix>` filters. Metrics continue to reflect the full
152
+ buffered stream so you can inspect a subset without losing session totals.
153
+ - Export: press `e` while the view is active to write the full buffered event
154
+ stream as pretty-printed JSON to `session-log-<timestamp>.json` in the
155
+ current working directory.
package/dist/app.d.ts CHANGED
@@ -1,9 +1,27 @@
1
- import type { AgentMuxClient } from '@a5c-ai/agent-mux';
1
+ import type { AgentMuxClient, DeferredPromptTarget, RunHandle } from '@a5c-ai/agent-mux';
2
2
  import type { TuiPlugin } from './plugin.js';
3
+ import type { KanbanControlPlane } from './kanban-control-plane.js';
3
4
  export interface AppProps {
4
5
  client: AgentMuxClient;
5
6
  plugins: TuiPlugin[];
7
+ kanban?: KanbanControlPlane;
6
8
  defaultAgent?: string;
9
+ initialViewId?: string;
10
+ disableChatAutoPrompt?: boolean;
7
11
  }
8
- export declare function App({ client, plugins, defaultAgent }: AppProps): import("react/jsx-runtime").JSX.Element;
9
- //# sourceMappingURL=app.d.ts.map
12
+ type ActiveRunCommand = {
13
+ kind: 'send';
14
+ prompt: string;
15
+ } | {
16
+ kind: 'queue';
17
+ prompt: string;
18
+ when: DeferredPromptTarget;
19
+ } | {
20
+ kind: 'steer';
21
+ prompt: string;
22
+ when: DeferredPromptTarget;
23
+ };
24
+ export declare function parseActiveRunCommand(prompt: string): ActiveRunCommand | null;
25
+ export declare function dispatchPromptToActiveRun(handle: RunHandle, prompt: string): Promise<string>;
26
+ export declare function App({ client, plugins, kanban, defaultAgent, initialViewId, disableChatAutoPrompt, }: AppProps): import("react/jsx-runtime").JSX.Element;
27
+ export {};
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAyB,MAAM,mBAAmB,CAAC;AAE/E,OAAO,KAAK,EAAE,SAAS,EAA+B,MAAM,aAAa,CAAC;AAO1E,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAQD,wBAAgB,GAAG,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,YAAuB,EAAE,EAAE,QAAQ,2CA2iBzE"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAc,oBAAoB,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAErG,OAAO,KAAK,EAAE,SAAS,EAA+B,MAAM,aAAa,CAAC;AAa1E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,KAAK,gBAAgB,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,oBAAoB,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAElE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAc7E;AAeD,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAelG;AA6BD,wBAAgB,GAAG,CAAC,EAClB,MAAM,EACN,OAAO,EACP,MAAM,EACN,YAAuB,EACvB,aAAsB,EACtB,qBAA6B,GAC9B,EAAE,QAAQ,2CA4rBV"}
package/dist/app.js CHANGED
@@ -1,24 +1,96 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { useEffect, useMemo, useState } from 'react';
3
- import { Box, Text, useInput, useApp } from 'ink';
3
+ import { Box, Text, useApp, useInput, useStdout } from 'ink';
4
4
  import { logger } from '@a5c-ai/agent-mux-observability';
5
5
  import { createRegistry, createContext, loadPlugins } from './registry.js';
6
6
  import { EventStream } from './event-stream.js';
7
- import { PromptInput } from './prompt-input.js';
7
+ import { PromptInput, createEmptyPromptInputState, insertPromptInput, } from './prompt-input.js';
8
8
  import { CommandPalette } from './command-palette.js';
9
9
  import { ModelPicker } from './model-picker.js';
10
10
  import { loadHistory, appendHistory } from './prompt-history-store.js';
11
+ import { resolveSessionDispatchPlan } from './session-dispatch.js';
12
+ import { createViewport, packSegments } from './layout.js';
13
+ export function parseActiveRunCommand(prompt) {
14
+ if (prompt.startsWith('/queue ')) {
15
+ const body = prompt.slice('/queue '.length).trim();
16
+ return body ? { kind: 'queue', prompt: body, when: 'next-turn' } : null;
17
+ }
18
+ if (prompt.startsWith('/steer-tool ')) {
19
+ const body = prompt.slice('/steer-tool '.length).trim();
20
+ return body ? { kind: 'steer', prompt: body, when: 'after-tool' } : null;
21
+ }
22
+ if (prompt.startsWith('/steer ')) {
23
+ const body = prompt.slice('/steer '.length).trim();
24
+ return body ? { kind: 'steer', prompt: body, when: 'after-response' } : null;
25
+ }
26
+ return { kind: 'send', prompt };
27
+ }
28
+ function describeDeferredTarget(target) {
29
+ switch (target) {
30
+ case 'after-tool':
31
+ return 'the next tool result';
32
+ case 'after-response':
33
+ return 'the next agent response';
34
+ case 'next-turn':
35
+ return 'the next turn';
36
+ default:
37
+ return 'a later event';
38
+ }
39
+ }
40
+ export async function dispatchPromptToActiveRun(handle, prompt) {
41
+ const command = parseActiveRunCommand(prompt);
42
+ if (!command) {
43
+ return 'Active-run command requires a message.';
44
+ }
45
+ if (command.kind === 'queue') {
46
+ await handle.queue(command.prompt, { when: command.when });
47
+ return `Queued for ${describeDeferredTarget(command.when)}…`;
48
+ }
49
+ if (command.kind === 'steer') {
50
+ await handle.steer(command.prompt, { when: command.when });
51
+ return `Steering after ${describeDeferredTarget(command.when)}…`;
52
+ }
53
+ await handle.send(command.prompt);
54
+ return 'Sending to active run…';
55
+ }
11
56
  function pickRenderers(renderers, ev) {
12
57
  const specific = renderers.find((r) => r.id !== 'fallback' && r.match(ev));
13
58
  if (specific)
14
59
  return specific;
15
60
  return renderers.find((r) => r.id === 'fallback');
16
61
  }
17
- export function App({ client, plugins, defaultAgent = 'claude' }) {
62
+ function SegmentLines({ lines }) {
63
+ return (_jsx(Box, { flexDirection: "column", children: lines.map((line, lineIndex) => (_jsx(Box, { children: line.map((segment, segmentIndex) => (_jsxs(Text, { color: segment.color, dimColor: segment.dimColor, children: [segmentIndex > 0 ? ' · ' : '', segment.text] }, segment.key))) }, `line:${lineIndex}`))) }));
64
+ }
65
+ export function App({ client, plugins, kanban, defaultAgent = 'claude', initialViewId = 'chat', disableChatAutoPrompt = false, }) {
18
66
  const { exit } = useApp();
67
+ const { stdout } = useStdout();
68
+ const [stdoutDimensions, setStdoutDimensions] = useState(() => ({
69
+ width: stdout?.columns ?? 80,
70
+ height: stdout?.rows ?? 24,
71
+ }));
72
+ useEffect(() => {
73
+ if (!stdout)
74
+ return;
75
+ const sync = () => {
76
+ setStdoutDimensions({
77
+ width: stdout.columns ?? 80,
78
+ height: stdout.rows ?? 24,
79
+ });
80
+ };
81
+ sync();
82
+ stdout.on('resize', sync);
83
+ return () => {
84
+ stdout.off('resize', sync);
85
+ };
86
+ }, [stdout]);
87
+ const viewport = useMemo(() => createViewport(stdoutDimensions.width, stdoutDimensions.height), [stdoutDimensions.height, stdoutDimensions.width]);
19
88
  const [status, setStatus] = useState('');
20
- const [activeId, setActiveId] = useState('chat');
89
+ const [pluginLoadVersion, setPluginLoadVersion] = useState(0);
90
+ const [activeId, setActiveId] = useState(initialViewId);
21
91
  const [promptMode, setPromptMode] = useState(false);
92
+ const [chatPromptDismissed, setChatPromptDismissed] = useState(false);
93
+ const [chatPromptState, setChatPromptState] = useState(() => createEmptyPromptInputState());
22
94
  const [pendingResume, setPendingResume] = useState(null);
23
95
  const currentHandleRef = React.useRef(null);
24
96
  const pendingApprovalRef = React.useRef(null);
@@ -27,6 +99,9 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
27
99
  const [filter, setFilter] = useState('');
28
100
  const [paletteMode, setPaletteMode] = useState(false);
29
101
  const [selection, setSelection] = useState(undefined);
102
+ const [issueSelection, setIssueSelection] = useState(undefined);
103
+ const [workspaceSelection, setWorkspaceSelection] = useState(undefined);
104
+ const [returnViewId, setReturnViewId] = useState('sessions');
30
105
  const [modelPickerMode, setModelPickerMode] = useState(false);
31
106
  const [currentModel, setCurrentModel] = useState(undefined);
32
107
  const [agentPickerMode, setAgentPickerMode] = useState(false);
@@ -59,6 +134,7 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
59
134
  deny: 'magenta',
60
135
  };
61
136
  const [execMode, setExecMode] = useState('normal');
137
+ const activeViewIdRef = React.useRef(initialViewId);
62
138
  function cycleExecMode() {
63
139
  setExecMode((m) => {
64
140
  const i = EXEC_MODES.indexOf(m);
@@ -85,28 +161,35 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
85
161
  const r = createRegistry();
86
162
  const s = new EventStream();
87
163
  const ctx = createContext(client, r, (ev) => {
88
- if (ev.type === 'status')
89
- setStatus(ev.message);
90
- if (ev.type === 'view:switch')
91
- setActiveId(ev.id);
92
- if (ev.type === 'session:select') {
93
- setPendingResume({ agent: ev.agent, sessionId: ev.sessionId });
94
- setStatus(`Resuming ${ev.agent}/${ev.sessionId} — type to send next message`);
95
- void loadSessionTranscript(ev.agent, ev.sessionId);
96
- }
97
- if (ev.type === 'session:detail') {
98
- setSelection({ agent: ev.agent, sessionId: ev.sessionId });
99
- }
100
- if (ev.type === 'session:diff') {
101
- void handleSessionDiff(ev.agent, ev.sessionId);
102
- }
103
- }, s);
104
- void loadPlugins(plugins, ctx);
164
+ void handleInternalEvent(ev, activeViewIdRef.current);
165
+ }, s, kanban);
166
+ void loadPlugins(plugins, ctx).then(() => {
167
+ setPluginLoadVersion((version) => version + 1);
168
+ });
105
169
  return { registry: r, stream: s };
106
170
  }, [client, plugins]);
171
+ const active = registry.views.find((v) => v.id === activeId) ?? registry.views[0];
172
+ activeViewIdRef.current = active?.id ?? activeId;
107
173
  useInput((input, key) => {
108
174
  if (promptMode || filterMode || paletteMode || modelPickerMode || profilePickerMode || agentPickerMode)
109
175
  return; // child input owns keys while open
176
+ if (active?.id === 'chat' && chatPromptDismissed) {
177
+ if (key.escape) {
178
+ setChatPromptDismissed(false);
179
+ setChatPromptState(createEmptyPromptInputState());
180
+ const fallbackView = registry.views.find((view) => view.id !== 'chat');
181
+ if (fallbackView) {
182
+ setActiveId(fallbackView.id);
183
+ }
184
+ return;
185
+ }
186
+ if (input && !key.ctrl && !key.meta) {
187
+ setChatPromptState((current) => insertPromptInput(current, input));
188
+ setChatPromptDismissed(false);
189
+ setPromptMode(true);
190
+ return;
191
+ }
192
+ }
110
193
  if (input === 'q' || (key.ctrl && input === 'c')) {
111
194
  exit();
112
195
  return;
@@ -175,24 +258,24 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
175
258
  registerEventRenderer: () => { },
176
259
  registerCommand: () => { },
177
260
  registerPromptHandler: () => { },
178
- emit: (e) => {
179
- if (e.type === 'status')
180
- setStatus(e.message);
181
- if (e.type === 'event')
182
- stream.push(e.event);
183
- },
261
+ emit: (e) => void handleInternalEvent(e, activeViewIdRef.current),
184
262
  });
185
263
  }
186
264
  }
187
265
  });
188
- const active = registry.views.find((v) => v.id === activeId) ?? registry.views[0];
266
+ const navLines = useMemo(() => packSegments(registry.views.map((v) => ({
267
+ key: v.id,
268
+ text: `[${v.hotkey ?? '?'}] ${v.title}`,
269
+ color: v.id === active?.id ? 'green' : 'gray',
270
+ })), viewport.width), [active?.id, registry.views, viewport.width]);
189
271
  // Chat view has an always-on inline prompt. Auto-focus when entering chat
190
- // and no modal is open. Esc inside PromptInput flips promptMode off, which
191
- // re-enables global hotkeys; any hotkey that switches away from chat also
192
- // clears it. This is the simplest model that keeps global navigation working.
272
+ // and no modal is open. The draft itself lives at the app level so Esc can
273
+ // temporarily dismiss the composer without losing the current prompt.
193
274
  useEffect(() => {
194
- if (active?.id === 'chat' &&
275
+ if (!disableChatAutoPrompt &&
276
+ active?.id === 'chat' &&
195
277
  !promptMode &&
278
+ !chatPromptDismissed &&
196
279
  !filterMode &&
197
280
  !paletteMode &&
198
281
  !modelPickerMode &&
@@ -200,31 +283,59 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
200
283
  !agentPickerMode) {
201
284
  setPromptMode(true);
202
285
  }
203
- if (active?.id !== 'chat' && promptMode) {
204
- setPromptMode(false);
286
+ if (active?.id !== 'chat') {
287
+ if (promptMode) {
288
+ setPromptMode(false);
289
+ }
290
+ if (chatPromptDismissed) {
291
+ setChatPromptDismissed(false);
292
+ }
205
293
  }
206
- }, [active?.id, filterMode, paletteMode, modelPickerMode, profilePickerMode, agentPickerMode, promptMode]);
294
+ }, [active?.id, chatPromptDismissed, disableChatAutoPrompt, filterMode, paletteMode, modelPickerMode, profilePickerMode, agentPickerMode, promptMode]);
207
295
  const ActiveView = active?.component;
208
296
  const viewEmit = (ev) => {
209
- if (ev.type === 'status')
297
+ void handleInternalEvent(ev, active?.id);
298
+ };
299
+ async function handleInternalEvent(ev, sourceViewId) {
300
+ if (ev.type === 'status') {
210
301
  setStatus(ev.message);
211
- else if (ev.type === 'view:switch')
302
+ return;
303
+ }
304
+ if (ev.type === 'event') {
305
+ stream.push(ev.event);
306
+ return;
307
+ }
308
+ if (ev.type === 'view:switch') {
212
309
  setActiveId(ev.id);
213
- else if (ev.type === 'session:select') {
310
+ return;
311
+ }
312
+ if (ev.type === 'issue:select') {
313
+ setIssueSelection({ issueId: ev.issueId, projectId: ev.projectId });
314
+ setActiveId(ev.viewId ?? 'kanban');
315
+ return;
316
+ }
317
+ if (ev.type === 'workspace:select') {
318
+ setWorkspaceSelection({ workspacePath: ev.workspacePath });
319
+ setActiveId(ev.viewId ?? 'workspaces');
320
+ return;
321
+ }
322
+ if (ev.type === 'session:select') {
214
323
  setPendingResume({ agent: ev.agent, sessionId: ev.sessionId });
215
324
  setStatus(`Resuming ${ev.agent}/${ev.sessionId} — type to send next message`);
216
- void loadSessionTranscript(ev.agent, ev.sessionId);
325
+ await loadSessionTranscript(ev.agent, ev.sessionId);
217
326
  setActiveId('chat');
327
+ return;
218
328
  }
219
- else if (ev.type === 'session:detail') {
329
+ if (ev.type === 'session:detail') {
330
+ const nextReturnViewId = sourceViewId && sourceViewId !== 'session-detail' ? sourceViewId : activeViewIdRef.current;
220
331
  setSelection({ agent: ev.agent, sessionId: ev.sessionId });
332
+ setReturnViewId(nextReturnViewId && nextReturnViewId !== 'session-detail' ? nextReturnViewId : 'sessions');
333
+ return;
221
334
  }
222
- else if (ev.type === 'session:diff') {
223
- void handleSessionDiff(ev.agent, ev.sessionId);
335
+ if (ev.type === 'session:diff') {
336
+ await handleSessionDiff(ev.agent, ev.sessionId);
224
337
  }
225
- else if (ev.type === 'event')
226
- stream.push(ev.event);
227
- };
338
+ }
228
339
  async function loadSessionTranscript(agent, sessionId) {
229
340
  try {
230
341
  const full = await client.sessions.get(agent, sessionId);
@@ -235,7 +346,7 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
235
346
  stream.push({
236
347
  runId: 'transcript',
237
348
  agent,
238
- timestamp: (m.timestamp ?? new Date()).toISOString(),
349
+ timestamp: new Date(m.timestamp ?? Date.now()).toISOString(),
239
350
  type: 'text_delta',
240
351
  delta: `[${m.role}] ${m.content}\n`,
241
352
  });
@@ -260,7 +371,7 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
260
371
  const sum = result.summary;
261
372
  const head = result.operations
262
373
  .slice(0, 30)
263
- .map((o, i) => `${i + 1}. ${o.kind ?? 'op'}`)
374
+ .map((o, i) => `${i + 1}. ${o.type}`)
264
375
  .join('\n');
265
376
  const text = `\n--- session diff ---\n` +
266
377
  `A: ${left.agent}/${left.sessionId}\n` +
@@ -293,6 +404,7 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
293
404
  async function handlePromptSubmit(prompt) {
294
405
  if (!prompt.trim())
295
406
  return;
407
+ setChatPromptDismissed(false);
296
408
  setPromptHistory((h) => {
297
409
  const next = h.filter((p) => p !== prompt);
298
410
  next.push(prompt);
@@ -309,8 +421,7 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
309
421
  // fresh process — keeps follow-ups inside the same session & context.
310
422
  if (currentHandleRef.current) {
311
423
  try {
312
- setStatus('Sending to active run…');
313
- await currentHandleRef.current.send(prompt);
424
+ setStatus(await dispatchPromptToActiveRun(currentHandleRef.current, prompt));
314
425
  }
315
426
  catch (e) {
316
427
  setStatus(`send failed: ${e.message}`);
@@ -323,19 +434,20 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
323
434
  ? (await client.profiles.apply(currentProfile))
324
435
  : {};
325
436
  const selectedAgent = currentModel?.agent ?? currentAgent ?? baseOpts.agent ?? defaultAgent;
326
- // TODO: cross-harness resume/fork if the user picked a different
327
- // agent than the session's origin we'd need a transcript-export /
328
- // re-import path. For now, refuse explicitly.
329
- if (pendingResume && pendingResume.agent !== selectedAgent && (currentModel || currentAgent)) {
330
- setStatus(`Cannot resume ${pendingResume.agent} session with ${selectedAgent}: cross-harness resume not implemented yet.`);
331
- return;
332
- }
333
- const runOpts = {
334
- agent: pendingResume?.agent ?? selectedAgent,
437
+ const explicitHarnessSelection = Boolean(currentModel || currentAgent);
438
+ const requestedAgent = pendingResume && !explicitHarnessSelection ? pendingResume.agent : selectedAgent;
439
+ const sessionPlan = await resolveSessionDispatchPlan({
440
+ sessions: client.sessions,
441
+ pendingResume,
442
+ requestedAgent,
335
443
  prompt,
444
+ });
445
+ const runOpts = {
446
+ agent: sessionPlan.agent,
447
+ prompt: sessionPlan.prompt,
336
448
  };
337
- if (pendingResume)
338
- runOpts.sessionId = pendingResume.sessionId;
449
+ if (sessionPlan.sessionId)
450
+ runOpts.sessionId = sessionPlan.sessionId;
339
451
  if (currentModel)
340
452
  runOpts.model = currentModel.modelId;
341
453
  else if (baseOpts.model)
@@ -354,6 +466,9 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
354
466
  }
355
467
  else
356
468
  runOpts.approvalMode = 'prompt';
469
+ if (sessionPlan.migration) {
470
+ setStatus(`Forking ${sessionPlan.migration.sourceAgent}/${sessionPlan.migration.sourceSessionId} into ${sessionPlan.agent} via transcript import…`);
471
+ }
357
472
  const resumedAgent = runOpts.agent;
358
473
  let resumedSessionId = runOpts.sessionId;
359
474
  const handle = client.run(runOpts);
@@ -406,31 +521,91 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
406
521
  }
407
522
  }
408
523
  }
409
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: registry.views.map((v) => (_jsxs(Text, { color: v.id === active?.id ? 'green' : 'gray', children: ["[", v.hotkey ?? '?', "] ", v.title, ' '] }, v.id))) }), _jsx(Box, { borderStyle: "single", flexDirection: "column", paddingX: 1, children: ViewWithRenderers ? (_jsx(ViewWithRenderers, { client: client, active: true, eventStream: stream, emit: viewEmit, filter: filter || undefined, selection: selection, activeSessions: activeSessions })) : (_jsx(Text, { dimColor: true, children: "No views registered." })) }), pendingApproval ? (_jsx(Box, { children: _jsxs(Text, { color: pendingApproval.riskLevel === 'high' ? 'red' : 'yellow', children: ["[approval ", pendingApproval.riskLevel, "] ", pendingApproval.action, " \u2014 y: approve \u00B7 n: deny"] }) })) : null, filterMode ? (_jsx(PromptInput, { label: "filter (substring or `type:<prefix>`)> ", onSubmit: (v) => {
524
+ const footerSegments = [
525
+ {
526
+ key: 'controls',
527
+ text: viewport.isVeryNarrow
528
+ ? '/ filter, : palette'
529
+ : 'shift+tab: mode, /: filter, :: palette',
530
+ dimColor: true,
531
+ },
532
+ { key: 'mode', text: `mode=${execMode}`, color: EXEC_MODE_COLORS[execMode] },
533
+ { key: 'agent', text: `agent=${currentAgent ?? defaultAgent}`, color: 'cyan' },
534
+ ];
535
+ if (filter) {
536
+ footerSegments.push({
537
+ key: 'filter',
538
+ text: viewport.isVeryNarrow ? `filter=${filter}` : `filter="${filter}"`,
539
+ color: 'cyan',
540
+ });
541
+ }
542
+ if (currentModel) {
543
+ footerSegments.push({
544
+ key: 'model',
545
+ text: `model=${currentModel.agent}/${currentModel.modelId}`,
546
+ color: 'magenta',
547
+ });
548
+ }
549
+ if (currentProfile) {
550
+ footerSegments.push({ key: 'profile', text: `profile=${currentProfile}`, color: 'blue' });
551
+ }
552
+ if (currentHandleRef.current) {
553
+ footerSegments.push({ key: 'interrupt', text: 'i: interrupt', color: 'yellow' });
554
+ if (!viewport.isVeryNarrow) {
555
+ footerSegments.push({
556
+ key: 'deferred',
557
+ text: '/queue, /steer, /steer-tool',
558
+ color: 'yellow',
559
+ });
560
+ }
561
+ }
562
+ if (pendingApproval) {
563
+ footerSegments.push({ key: 'approval', text: 'y/n: approve', color: 'yellow' });
564
+ }
565
+ footerSegments.push({ key: 'quit', text: 'q: quit', dimColor: true });
566
+ if (!viewport.isVeryNarrow && registry.views.length > 1) {
567
+ footerSegments.push({
568
+ key: 'views',
569
+ text: registry.views
570
+ .filter((v) => v.hotkey)
571
+ .map((v) => `${v.hotkey}:${v.title.toLowerCase()}`)
572
+ .join(' '),
573
+ dimColor: true,
574
+ });
575
+ }
576
+ if (!viewport.isShort && !viewport.isVeryNarrow && registry.commands.length > 0) {
577
+ footerSegments.push({
578
+ key: 'commands',
579
+ text: registry.commands.map((c) => `${c.hotkey}:${c.label}`).join(' '),
580
+ dimColor: true,
581
+ });
582
+ }
583
+ const footerLines = packSegments(footerSegments, viewport.width);
584
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SegmentLines, { lines: navLines }), _jsx(Box, { borderStyle: "single", flexDirection: "column", paddingX: 1, children: ViewWithRenderers ? (_jsx(ViewWithRenderers, { client: client, kanban: kanban, active: true, eventStream: stream, emit: viewEmit, pluginEpoch: pluginLoadVersion, viewport: viewport, filter: filter || undefined, selection: selection, issueSelection: issueSelection, workspaceSelection: workspaceSelection, returnViewId: returnViewId, activeSessions: activeSessions })) : (_jsx(Text, { dimColor: true, children: "No views registered." })) }), pendingApproval ? (_jsx(Box, { children: _jsxs(Text, { color: pendingApproval.riskLevel === 'high' ? 'red' : 'yellow', children: ["[approval ", pendingApproval.riskLevel, "] ", pendingApproval.action, " \u2014 y: approve \u00B7 n: deny"] }) })) : null, filterMode ? (_jsx(PromptInput, { label: "filter (substring or `type:<prefix>`)> ", onSubmit: (v) => {
410
585
  setFilter(v);
411
586
  setFilterMode(false);
412
587
  setStatus(v ? `Filter: ${v}` : 'Filter cleared.');
413
- }, onCancel: () => setFilterMode(false) })) : null, profilePickerMode ? (_jsx(ModelPicker, { models: availableProfiles.map((n) => ({ agent: 'profile', modelId: n })), onCancel: () => setProfilePickerMode(false), onPick: (m) => {
588
+ }, onCancel: () => setFilterMode(false) })) : null, profilePickerMode ? (_jsx(ModelPicker, { models: availableProfiles.map((n) => ({ agent: 'profile', modelId: n })), maxItems: viewport.overlayRowLimit, width: viewport.contentWidth, title: "profile", onCancel: () => setProfilePickerMode(false), onPick: (m) => {
414
589
  setCurrentProfile(m.modelId);
415
590
  setProfilePickerMode(false);
416
591
  setStatus(`Profile: ${m.modelId}`);
417
- } })) : null, modelPickerMode ? (_jsx(ModelPicker, { models: availableModels, onCancel: () => setModelPickerMode(false), onPick: (m) => {
592
+ } })) : null, modelPickerMode ? (_jsx(ModelPicker, { models: availableModels, maxItems: viewport.overlayRowLimit, width: viewport.contentWidth, onCancel: () => setModelPickerMode(false), onPick: (m) => {
418
593
  setCurrentModel(m);
419
594
  setModelPickerMode(false);
420
595
  setStatus(`Model: ${m.agent}/${m.modelId}`);
421
- } })) : null, agentPickerMode ? (_jsx(ModelPicker, { models: (() => {
596
+ } })) : null, agentPickerMode ? (_jsx(ModelPicker, { title: "agent", models: (() => {
422
597
  try {
423
598
  return client.adapters.list().map((a) => ({ agent: a.agent, modelId: a.displayName }));
424
599
  }
425
600
  catch {
426
601
  return [];
427
602
  }
428
- })(), onCancel: () => setAgentPickerMode(false), onPick: (m) => {
603
+ })(), maxItems: viewport.overlayRowLimit, width: viewport.contentWidth, onCancel: () => setAgentPickerMode(false), onPick: (m) => {
429
604
  setCurrentAgent(m.agent);
430
605
  setCurrentModel(undefined);
431
606
  setAgentPickerMode(false);
432
607
  setStatus(`Agent: ${m.agent}`);
433
- } })) : null, paletteMode ? (_jsx(CommandPalette, { views: registry.views, commands: registry.commands, onCancel: () => setPaletteMode(false), onPick: (a) => {
608
+ } })) : null, paletteMode ? (_jsx(CommandPalette, { views: registry.views, commands: registry.commands, maxItems: viewport.overlayRowLimit, width: viewport.contentWidth, onCancel: () => setPaletteMode(false), onPick: (a) => {
434
609
  setPaletteMode(false);
435
610
  if (a.id.startsWith('view:')) {
436
611
  setActiveId(a.id.slice('view:'.length));
@@ -446,18 +621,15 @@ export function App({ client, plugins, defaultAgent = 'claude' }) {
446
621
  registerEventRenderer: () => { },
447
622
  registerCommand: () => { },
448
623
  registerPromptHandler: () => { },
449
- emit: (e) => {
450
- if (e.type === 'status')
451
- setStatus(e.message);
452
- if (e.type === 'event')
453
- stream.push(e.event);
454
- },
624
+ emit: (e) => void handleInternalEvent(e, activeViewIdRef.current),
455
625
  });
456
626
  }
457
627
  }
458
- } })) : null, _jsxs(Box, { flexDirection: "column", children: [status ? _jsx(Text, { dimColor: true, children: status }) : null, _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "shift+tab: mode \u00B7 /: filter \u00B7 :: palette \u00B7 m: model \u00B7 N: agent \u00B7 P: profile" }), _jsxs(Text, { color: EXEC_MODE_COLORS[execMode], children: [" \u00B7 mode=", execMode] }), _jsxs(Text, { color: "cyan", children: [" \u00B7 agent=", currentAgent ?? defaultAgent] }), filter ? _jsxs(Text, { color: "cyan", children: [" \u00B7 filter=\"", filter, "\""] }) : null, currentModel ? (_jsxs(Text, { color: "magenta", children: [" \u00B7 model=", currentModel.agent, "/", currentModel.modelId] })) : null, currentProfile ? (_jsxs(Text, { color: "blue", children: [" \u00B7 profile=", currentProfile] })) : null, currentHandleRef.current ? _jsx(Text, { color: "yellow", children: " \u00B7 i: interrupt" }) : null, pendingApproval ? _jsx(Text, { color: "yellow", children: " \u00B7 y/n: approve/deny" }) : null, _jsx(Text, { dimColor: true, children: " \u00B7 q: quit" }), registry.views.length > 1 ? (_jsxs(Text, { dimColor: true, children: [' · ', registry.views
459
- .filter((v) => v.hotkey)
460
- .map((v) => `${v.hotkey}:${v.title.toLowerCase()}`)
461
- .join(' ')] })) : null, registry.commands.length > 0 ? (_jsxs(Text, { dimColor: true, children: [' · ', registry.commands.map((c) => `${c.hotkey}:${c.label}`).join(' ')] })) : null] }), promptMode ? (_jsx(PromptInput, { label: "> ", labelColor: EXEC_MODE_COLORS[execMode], onSubmit: handlePromptSubmit, onCancel: () => setPromptMode(false), onShiftTab: cycleExecMode, history: promptHistory })) : null] })] }));
628
+ } })) : null, _jsxs(Box, { flexDirection: "column", children: [status ? _jsx(Text, { dimColor: true, children: status }) : null, _jsx(SegmentLines, { lines: footerLines }), promptMode ? (_jsx(PromptInput, { label: "> ", labelColor: EXEC_MODE_COLORS[execMode], onSubmit: handlePromptSubmit, initialState: chatPromptState, onStateChange: setChatPromptState, onCancel: () => {
629
+ setPromptMode(false);
630
+ if (active?.id === 'chat') {
631
+ setChatPromptDismissed(true);
632
+ }
633
+ }, onShiftTab: cycleExecMode, history: promptHistory })) : null] })] }));
462
634
  }
463
635
  //# sourceMappingURL=app.js.map