@robota-sdk/agent-transport 3.0.0-beta.64

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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/dist/node/headless/index.cjs +1 -0
  3. package/dist/node/headless/index.d.ts +2 -0
  4. package/dist/node/headless/index.js +1 -0
  5. package/dist/node/headless-CWEpJXFK.js +7 -0
  6. package/dist/node/headless-CWEpJXFK.js.map +1 -0
  7. package/dist/node/headless-CsZFelG9.cjs +6 -0
  8. package/dist/node/http/index.cjs +1 -0
  9. package/dist/node/http/index.d.ts +2 -0
  10. package/dist/node/http/index.js +1 -0
  11. package/dist/node/http-CM3TJhrF.cjs +1 -0
  12. package/dist/node/http-DwO1AHG-.js +2 -0
  13. package/dist/node/http-DwO1AHG-.js.map +1 -0
  14. package/dist/node/index--Ti9NzQX.d.ts +64 -0
  15. package/dist/node/index--Ti9NzQX.d.ts.map +1 -0
  16. package/dist/node/index-B_rcr14p.d.ts +47 -0
  17. package/dist/node/index-B_rcr14p.d.ts.map +1 -0
  18. package/dist/node/index-C9LWCL4l.d.ts +34 -0
  19. package/dist/node/index-C9LWCL4l.d.ts.map +1 -0
  20. package/dist/node/index-CAr3ioVh.d.ts +64 -0
  21. package/dist/node/index-CAr3ioVh.d.ts.map +1 -0
  22. package/dist/node/index-CEs25wVk.d.ts +213 -0
  23. package/dist/node/index-CEs25wVk.d.ts.map +1 -0
  24. package/dist/node/index-CvXLpjJO.d.ts +213 -0
  25. package/dist/node/index-CvXLpjJO.d.ts.map +1 -0
  26. package/dist/node/index-D34WUfFH.d.ts +26 -0
  27. package/dist/node/index-D34WUfFH.d.ts.map +1 -0
  28. package/dist/node/index-Y0zHb1Bz.d.ts +47 -0
  29. package/dist/node/index-Y0zHb1Bz.d.ts.map +1 -0
  30. package/dist/node/index-k3TUjA-T.d.ts +26 -0
  31. package/dist/node/index-k3TUjA-T.d.ts.map +1 -0
  32. package/dist/node/index-nBlMTFkZ.d.ts +34 -0
  33. package/dist/node/index-nBlMTFkZ.d.ts.map +1 -0
  34. package/dist/node/index.cjs +1 -0
  35. package/dist/node/index.d.ts +6 -0
  36. package/dist/node/index.js +1 -0
  37. package/dist/node/mcp/index.cjs +1 -0
  38. package/dist/node/mcp/index.d.ts +2 -0
  39. package/dist/node/mcp/index.js +1 -0
  40. package/dist/node/mcp-BXBwF6Wu.js +2 -0
  41. package/dist/node/mcp-BXBwF6Wu.js.map +1 -0
  42. package/dist/node/mcp-DcHuGokt.cjs +1 -0
  43. package/dist/node/tui/index.cjs +1 -0
  44. package/dist/node/tui/index.d.ts +2 -0
  45. package/dist/node/tui/index.js +1 -0
  46. package/dist/node/tui-CeD_6rSo.cjs +24 -0
  47. package/dist/node/tui-zmDTPk4b.js +25 -0
  48. package/dist/node/tui-zmDTPk4b.js.map +1 -0
  49. package/dist/node/ws/index.cjs +1 -0
  50. package/dist/node/ws/index.d.ts +2 -0
  51. package/dist/node/ws/index.js +1 -0
  52. package/dist/node/ws-B-oRccFl.js +2 -0
  53. package/dist/node/ws-B-oRccFl.js.map +1 -0
  54. package/dist/node/ws-COnIgnmn.cjs +1 -0
  55. package/package.json +141 -0
  56. package/src/headless/__tests__/headless-runner-initialization.test.ts +45 -0
  57. package/src/headless/__tests__/headless-runner.test.ts +484 -0
  58. package/src/headless/__tests__/headless-skill-activation.integration.test.ts +430 -0
  59. package/src/headless/__tests__/headless-transport.test.ts +268 -0
  60. package/src/headless/headless-runner.ts +141 -0
  61. package/src/headless/headless-stream-json.ts +142 -0
  62. package/src/headless/headless-transport.ts +43 -0
  63. package/src/headless/index.ts +4 -0
  64. package/src/http/__tests__/http-transport.test.ts +55 -0
  65. package/src/http/__tests__/routes.test.ts +168 -0
  66. package/src/http/http-transport.ts +42 -0
  67. package/src/http/index.ts +4 -0
  68. package/src/http/routes.ts +151 -0
  69. package/src/index.ts +5 -0
  70. package/src/mcp/__tests__/mcp-server.test.ts +66 -0
  71. package/src/mcp/__tests__/mcp-transport.test.ts +46 -0
  72. package/src/mcp/index.ts +4 -0
  73. package/src/mcp/mcp-server.ts +162 -0
  74. package/src/mcp/mcp-transport.ts +48 -0
  75. package/src/tui/App.tsx +478 -0
  76. package/src/tui/BackgroundTaskPanel.tsx +34 -0
  77. package/src/tui/CjkTextInput.tsx +204 -0
  78. package/src/tui/ConfirmPrompt.tsx +69 -0
  79. package/src/tui/ExecutionWorkspaceDetailPane.tsx +62 -0
  80. package/src/tui/ExecutionWorkspaceSwitcher.tsx +185 -0
  81. package/src/tui/InkTerminal.ts +42 -0
  82. package/src/tui/InputArea.tsx +298 -0
  83. package/src/tui/InteractivePrompt.tsx +57 -0
  84. package/src/tui/ListPicker.tsx +94 -0
  85. package/src/tui/MenuSelect.tsx +103 -0
  86. package/src/tui/MessageList.tsx +282 -0
  87. package/src/tui/PermissionPrompt.tsx +84 -0
  88. package/src/tui/PluginTUI.tsx +256 -0
  89. package/src/tui/SessionPicker.tsx +66 -0
  90. package/src/tui/SessionStatusBar.tsx +66 -0
  91. package/src/tui/SlashAutocomplete.tsx +110 -0
  92. package/src/tui/StatusBar.tsx +213 -0
  93. package/src/tui/StreamingIndicator.tsx +91 -0
  94. package/src/tui/TextPrompt.tsx +80 -0
  95. package/src/tui/ToolCommandOutput.tsx +37 -0
  96. package/src/tui/ToolDiffBlock.tsx +30 -0
  97. package/src/tui/TransportTUI.tsx +116 -0
  98. package/src/tui/UpdateNotice.tsx +14 -0
  99. package/src/tui/UsageSummaryEntry.tsx +38 -0
  100. package/src/tui/WaveText.tsx +44 -0
  101. package/src/tui/__tests__/InteractivePrompt.test.tsx +82 -0
  102. package/src/tui/__tests__/ListPicker.test.tsx +159 -0
  103. package/src/tui/__tests__/MenuSelect.test.tsx +103 -0
  104. package/src/tui/__tests__/PluginTUI.test.tsx +167 -0
  105. package/src/tui/__tests__/SlashAutocomplete.test.tsx +140 -0
  106. package/src/tui/__tests__/TextPrompt.test.tsx +98 -0
  107. package/src/tui/__tests__/UpdateNotice.test.tsx +15 -0
  108. package/src/tui/__tests__/abort-after-permission.test.tsx +169 -0
  109. package/src/tui/__tests__/abort-streaming-e2e.test.tsx +183 -0
  110. package/src/tui/__tests__/background-task-panel.test.tsx +53 -0
  111. package/src/tui/__tests__/background-task-row-format.test.ts +59 -0
  112. package/src/tui/__tests__/cjk-text-input-flow.test.ts +109 -0
  113. package/src/tui/__tests__/cjk-text-input.test.ts +191 -0
  114. package/src/tui/__tests__/command-effect-handler.test.ts +128 -0
  115. package/src/tui/__tests__/command-output-summary.test.ts +95 -0
  116. package/src/tui/__tests__/compact-event-bridge.test.ts +20 -0
  117. package/src/tui/__tests__/confirm-permission-flow.test.ts +91 -0
  118. package/src/tui/__tests__/confirm-prompt.test.tsx +87 -0
  119. package/src/tui/__tests__/execution-workspace-switcher.test.tsx +110 -0
  120. package/src/tui/__tests__/execution-workspace-view-model.test.ts +93 -0
  121. package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +122 -0
  122. package/src/tui/__tests__/input-area-flow.test.ts +152 -0
  123. package/src/tui/__tests__/message-list-rendering.test.tsx +353 -0
  124. package/src/tui/__tests__/model-change-side-effect.test.ts +91 -0
  125. package/src/tui/__tests__/prompt-queue.test.tsx +255 -0
  126. package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +233 -0
  127. package/src/tui/__tests__/render-markdown.test.ts +72 -0
  128. package/src/tui/__tests__/selection-flow.test.ts +61 -0
  129. package/src/tui/__tests__/slash-routing-effects.test.ts +225 -0
  130. package/src/tui/__tests__/status-activity.test.ts +71 -0
  131. package/src/tui/__tests__/status-bar.test.tsx +157 -0
  132. package/src/tui/__tests__/streaming-indicator.test.tsx +137 -0
  133. package/src/tui/__tests__/text-prompt-flow.test.ts +77 -0
  134. package/src/tui/__tests__/tui-state-manager.test.ts +401 -0
  135. package/src/tui/background-task-row-format.ts +52 -0
  136. package/src/tui/command-output-summary.ts +122 -0
  137. package/src/tui/execution-workspace-view-model.ts +123 -0
  138. package/src/tui/flows/cjk-text-input-flow.ts +285 -0
  139. package/src/tui/flows/confirm-prompt-flow.ts +45 -0
  140. package/src/tui/flows/input-area-flow.ts +186 -0
  141. package/src/tui/flows/permission-prompt-flow.ts +76 -0
  142. package/src/tui/flows/selection-flow.ts +126 -0
  143. package/src/tui/flows/text-prompt-flow.ts +98 -0
  144. package/src/tui/hooks/command-effect-handler.ts +98 -0
  145. package/src/tui/hooks/command-effect-queue.ts +39 -0
  146. package/src/tui/hooks/model-change-side-effect.ts +63 -0
  147. package/src/tui/hooks/side-effects-types.ts +38 -0
  148. package/src/tui/hooks/use-interactive-session-init.ts +50 -0
  149. package/src/tui/hooks/useAutocomplete.ts +85 -0
  150. package/src/tui/hooks/useInteractiveSession.ts +273 -0
  151. package/src/tui/hooks/usePermissionQueue.ts +51 -0
  152. package/src/tui/hooks/usePluginCallbacks.ts +30 -0
  153. package/src/tui/hooks/usePluginScreenData.ts +84 -0
  154. package/src/tui/hooks/useSideEffects.ts +210 -0
  155. package/src/tui/hooks/useSlashRouting.ts +117 -0
  156. package/src/tui/hooks/useStatusLineSettings.ts +35 -0
  157. package/src/tui/index.ts +3 -0
  158. package/src/tui/plugin-tui-handlers.ts +163 -0
  159. package/src/tui/render-markdown.ts +129 -0
  160. package/src/tui/render.tsx +60 -0
  161. package/src/tui/status-activity.ts +63 -0
  162. package/src/tui/tui-cli-adapter-context.tsx +12 -0
  163. package/src/tui/tui-cli-adapter.ts +25 -0
  164. package/src/tui/tui-state-manager.ts +225 -0
  165. package/src/tui/tui-transport.ts +32 -0
  166. package/src/tui/types.ts +14 -0
  167. package/src/tui/utils/__tests__/edit-diff.test.ts +426 -0
  168. package/src/tui/utils/__tests__/paste-detection.test.ts +116 -0
  169. package/src/tui/utils/__tests__/paste-labels.test.ts +46 -0
  170. package/src/tui/utils/__tests__/tool-call-extractor.test.ts +227 -0
  171. package/src/tui/utils/__tests__/tool-diff-summary.test.ts +104 -0
  172. package/src/tui/utils/edit-diff.ts +152 -0
  173. package/src/tui/utils/paste-labels.ts +9 -0
  174. package/src/tui/utils/tool-call-extractor.ts +91 -0
  175. package/src/tui/utils/tool-diff-summary.ts +75 -0
  176. package/src/ws/__tests__/ws-handler.test.ts +407 -0
  177. package/src/ws/__tests__/ws-transport.test.ts +53 -0
  178. package/src/ws/index.ts +13 -0
  179. package/src/ws/ws-background-messages.ts +170 -0
  180. package/src/ws/ws-handler.ts +279 -0
  181. package/src/ws/ws-protocol.ts +76 -0
  182. package/src/ws/ws-transport-configurable.ts +123 -0
  183. package/src/ws/ws-transport.ts +42 -0
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Slash command routing logic for the TUI.
3
+ * Extracted from useInteractiveSession for single-responsibility.
4
+ */
5
+
6
+ import { useCallback } from 'react';
7
+ import type {
8
+ IInteractiveSession,
9
+ CommandRegistry,
10
+ ICommandResult,
11
+ TCommandEffect,
12
+ } from '@robota-sdk/agent-framework';
13
+ import { createSystemMessage, messageToHistoryEntry } from '@robota-sdk/agent-core';
14
+ import type { TuiStateManager } from '../tui-state-manager.js';
15
+ import type { ICommandEffectQueue } from './command-effect-queue.js';
16
+
17
+ export function useSlashRouting(
18
+ interactiveSession: IInteractiveSession,
19
+ registry: CommandRegistry,
20
+ manager: TuiStateManager,
21
+ commandEffectQueue: ICommandEffectQueue,
22
+ reloadPluginCommandSource?: (registry: CommandRegistry) => void,
23
+ ): (input: string) => Promise<void> {
24
+ return useCallback(
25
+ async (input: string) => {
26
+ if (!input.startsWith('/')) {
27
+ await interactiveSession.submit(input);
28
+ manager.setPendingPrompt(interactiveSession.getPendingPrompt());
29
+ return;
30
+ }
31
+
32
+ const parts = input.slice(1).split(/\s+/);
33
+ const cmd = parts[0]?.toLowerCase() ?? '';
34
+ const args = parts.slice(1).join(' ');
35
+
36
+ // Try system command first
37
+ const result = await interactiveSession.executeCommand(cmd, args);
38
+ if (result) {
39
+ if (result.effects?.some((effect) => effect.type === 'session-execution-started')) {
40
+ manager.setPendingPrompt(interactiveSession.getPendingPrompt());
41
+ return;
42
+ }
43
+ applySystemCommandResult(
44
+ result,
45
+ interactiveSession,
46
+ registry,
47
+ manager,
48
+ commandEffectQueue,
49
+ reloadPluginCommandSource,
50
+ );
51
+ return;
52
+ }
53
+
54
+ manager.addEntry(
55
+ messageToHistoryEntry(
56
+ createSystemMessage(`Unknown command "/${cmd}". Type /help for help.`),
57
+ ),
58
+ );
59
+ },
60
+ [interactiveSession, registry, manager, commandEffectQueue, reloadPluginCommandSource],
61
+ );
62
+ }
63
+
64
+ export function applySystemCommandResult(
65
+ result: ICommandResult,
66
+ interactiveSession: IInteractiveSession,
67
+ registry: CommandRegistry,
68
+ manager: TuiStateManager,
69
+ commandEffectQueue: ICommandEffectQueue,
70
+ reloadPluginCommandSource?: (registry: CommandRegistry) => void,
71
+ ): void {
72
+ const pendingEffects = applyImmediateCommandEffects(
73
+ result.effects,
74
+ registry,
75
+ manager,
76
+ reloadPluginCommandSource,
77
+ );
78
+ manager.addEntry(messageToHistoryEntry(createSystemMessage(result.message)));
79
+
80
+ if (result.interaction !== undefined) {
81
+ commandEffectQueue.enqueueInteraction(result.interaction);
82
+ }
83
+ if (pendingEffects.length > 0) {
84
+ commandEffectQueue.enqueueEffects(pendingEffects);
85
+ }
86
+
87
+ if (interactiveSession.isInitialized) {
88
+ const ctx = interactiveSession.getContextState();
89
+ manager.setContextState({
90
+ percentage: ctx.usedPercentage,
91
+ usedTokens: ctx.usedTokens,
92
+ maxTokens: ctx.maxTokens,
93
+ });
94
+ }
95
+ }
96
+
97
+ function applyImmediateCommandEffects(
98
+ effects: readonly TCommandEffect[] | undefined,
99
+ registry: CommandRegistry,
100
+ manager: TuiStateManager,
101
+ reloadPluginCommandSource?: (registry: CommandRegistry) => void,
102
+ ): TCommandEffect[] {
103
+ if (effects === undefined || effects.length === 0) return [];
104
+ const pendingEffects: TCommandEffect[] = [];
105
+ for (const effect of effects) {
106
+ if (effect.type === 'conversation-history-cleared') {
107
+ manager.clearHistory();
108
+ continue;
109
+ }
110
+ if (effect.type === 'plugin-registry-reload-requested') {
111
+ reloadPluginCommandSource?.(registry);
112
+ continue;
113
+ }
114
+ pendingEffects.push(effect);
115
+ }
116
+ return pendingEffects;
117
+ }
@@ -0,0 +1,35 @@
1
+ import { useState } from 'react';
2
+ import type { IStatusLineCommandSettings } from '@robota-sdk/agent-framework';
3
+ import { DEFAULT_STATUS_LINE_COMMAND_SETTINGS } from '@robota-sdk/agent-framework';
4
+ import { useTuiCliAdapter } from '../tui-cli-adapter-context.js';
5
+ import type { TUniversalValue } from '@robota-sdk/agent-core';
6
+
7
+ function readStatusLineSettings(
8
+ settings: Record<string, TUniversalValue>,
9
+ ): IStatusLineCommandSettings {
10
+ const defaults = { ...DEFAULT_STATUS_LINE_COMMAND_SETTINGS };
11
+ const raw = settings.statusline;
12
+ if (!isRecord(raw)) {
13
+ return defaults;
14
+ }
15
+ return {
16
+ enabled: typeof raw.enabled === 'boolean' ? raw.enabled : defaults.enabled,
17
+ gitBranch: typeof raw.gitBranch === 'boolean' ? raw.gitBranch : defaults.gitBranch,
18
+ };
19
+ }
20
+
21
+ function isRecord(value: TUniversalValue): value is Record<string, TUniversalValue> {
22
+ return (
23
+ value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)
24
+ );
25
+ }
26
+
27
+ export function useStatusLineSettings(): [
28
+ IStatusLineCommandSettings,
29
+ (settings: IStatusLineCommandSettings) => void,
30
+ ] {
31
+ const cliAdapter = useTuiCliAdapter();
32
+ return useState<IStatusLineCommandSettings>(() =>
33
+ readStatusLineSettings(cliAdapter.readSettings(cliAdapter.getUserSettingsPath())),
34
+ );
35
+ }
@@ -0,0 +1,3 @@
1
+ export { TuiTransport } from './tui-transport.js';
2
+ export type { ITuiCliAdapter } from './tui-cli-adapter.js';
3
+ export type { IRenderOptions } from './render.js';
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Screen-specific selection handlers for PluginTUI.
3
+ * Extracted to keep PluginTUI.tsx under 300 lines.
4
+ */
5
+
6
+ import type { IMenuSelectItem } from './MenuSelect.js';
7
+ import type { ICommandPluginAdapter } from '@robota-sdk/agent-framework';
8
+
9
+ interface IConfirmState {
10
+ message: string;
11
+ onConfirm: () => void;
12
+ onCancel: () => void;
13
+ }
14
+
15
+ interface IMenuContext {
16
+ marketplace?: string;
17
+ pluginId?: string;
18
+ }
19
+
20
+ interface INavActions {
21
+ push: (state: { screen: string; context?: IMenuContext }) => void;
22
+ pop: () => void;
23
+ popN: (n: number) => void;
24
+ notify: (content: string) => void;
25
+ setConfirm: (state: IConfirmState | undefined) => void;
26
+ refresh: () => void;
27
+ }
28
+
29
+ export function handleMainSelect(value: string, nav: Pick<INavActions, 'push'>): void {
30
+ if (value === 'marketplace') {
31
+ nav.push({ screen: 'marketplace-list' });
32
+ } else if (value === 'installed') {
33
+ nav.push({ screen: 'installed-list' });
34
+ }
35
+ }
36
+
37
+ export function handleMarketplaceListSelect(value: string, nav: Pick<INavActions, 'push'>): void {
38
+ if (value === '__add__') {
39
+ nav.push({ screen: 'marketplace-add' });
40
+ } else {
41
+ nav.push({ screen: 'marketplace-action', context: { marketplace: value } });
42
+ }
43
+ }
44
+
45
+ export function handleMarketplaceActionSelect(
46
+ value: string,
47
+ marketplace: string,
48
+ callbacks: ICommandPluginAdapter,
49
+ nav: Pick<INavActions, 'push' | 'pop' | 'popN' | 'notify' | 'setConfirm'>,
50
+ ): void {
51
+ if (value === 'browse') {
52
+ nav.push({ screen: 'marketplace-browse', context: { marketplace } });
53
+ } else if (value === 'update') {
54
+ callbacks
55
+ .marketplaceUpdate(marketplace)
56
+ .then(() => {
57
+ nav.notify(`Updated marketplace "${marketplace}".`);
58
+ nav.pop();
59
+ })
60
+ .catch((err) => {
61
+ nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
62
+ });
63
+ } else if (value === 'remove') {
64
+ nav.setConfirm({
65
+ message: `Remove marketplace "${marketplace}" and all its plugins?`,
66
+ onConfirm: () => {
67
+ nav.setConfirm(undefined);
68
+ callbacks
69
+ .marketplaceRemove(marketplace)
70
+ .then(() => {
71
+ nav.notify(`Removed marketplace "${marketplace}".`);
72
+ nav.popN(2);
73
+ })
74
+ .catch((err) => {
75
+ nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
76
+ });
77
+ },
78
+ onCancel: () => nav.setConfirm(undefined),
79
+ });
80
+ }
81
+ }
82
+
83
+ export function handleMarketplaceBrowseSelect(
84
+ value: string,
85
+ marketplace: string,
86
+ items: IMenuSelectItem[],
87
+ nav: Pick<INavActions, 'push'>,
88
+ ): void {
89
+ const fullId = `${value}@${marketplace}`;
90
+ const item = items.find((i) => i.value === value);
91
+ if (item?.hint === 'installed') {
92
+ nav.push({ screen: 'installed-action', context: { pluginId: fullId } });
93
+ } else {
94
+ nav.push({ screen: 'marketplace-install-scope', context: { marketplace, pluginId: fullId } });
95
+ }
96
+ }
97
+
98
+ export function handleInstallScopeSelect(
99
+ value: string,
100
+ pluginId: string,
101
+ callbacks: ICommandPluginAdapter,
102
+ nav: Pick<INavActions, 'popN' | 'notify'>,
103
+ ): void {
104
+ const scope = value as 'user' | 'project';
105
+ callbacks
106
+ .install(pluginId, scope)
107
+ .then(() => {
108
+ nav.notify(`Installed plugin "${pluginId}" (${scope} scope).`);
109
+ nav.popN(2);
110
+ })
111
+ .catch((err) => {
112
+ nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
113
+ });
114
+ }
115
+
116
+ export function handleInstalledListSelect(
117
+ value: string,
118
+ callbacks: ICommandPluginAdapter,
119
+ nav: Pick<INavActions, 'notify' | 'setConfirm' | 'refresh'>,
120
+ ): void {
121
+ nav.setConfirm({
122
+ message: `Uninstall plugin "${value}"?`,
123
+ onConfirm: () => {
124
+ nav.setConfirm(undefined);
125
+ callbacks
126
+ .uninstall(value)
127
+ .then(() => {
128
+ nav.notify(`Uninstalled plugin "${value}".`);
129
+ nav.refresh();
130
+ })
131
+ .catch((err) => {
132
+ nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
133
+ });
134
+ },
135
+ onCancel: () => nav.setConfirm(undefined),
136
+ });
137
+ }
138
+
139
+ export function handleInstalledActionSelect(
140
+ value: string,
141
+ pluginId: string,
142
+ callbacks: ICommandPluginAdapter,
143
+ nav: Pick<INavActions, 'popN' | 'notify' | 'setConfirm'>,
144
+ ): void {
145
+ if (value === 'uninstall') {
146
+ nav.setConfirm({
147
+ message: `Uninstall plugin "${pluginId}"?`,
148
+ onConfirm: () => {
149
+ nav.setConfirm(undefined);
150
+ callbacks
151
+ .uninstall(pluginId)
152
+ .then(() => {
153
+ nav.notify(`Uninstalled plugin "${pluginId}".`);
154
+ nav.popN(2);
155
+ })
156
+ .catch((err) => {
157
+ nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
158
+ });
159
+ },
160
+ onCancel: () => nav.setConfirm(undefined),
161
+ });
162
+ }
163
+ }
@@ -0,0 +1,129 @@
1
+ import { marked } from 'marked';
2
+ import type { Renderer } from 'marked';
3
+ // @ts-expect-error — marked-terminal has no type declarations
4
+ import TerminalRenderer from 'marked-terminal';
5
+
6
+ const ANSI_LIGHT_RED = '\u001b[38;5;210m';
7
+ const ANSI_LIGHT_GREEN = '\u001b[38;5;120m';
8
+ const ANSI_CYAN = '\u001b[36m';
9
+ const ANSI_DIM = '\u001b[2m';
10
+ const ANSI_DARK_RED_BACKGROUND = '\u001b[48;5;52m';
11
+ const ANSI_DARK_GREEN_BACKGROUND = '\u001b[48;5;22m';
12
+ const ANSI_RESET = '\u001b[0m';
13
+ const CODE_BLOCK_INDENT = ' ';
14
+ const ZERO_COLOR = '0';
15
+
16
+ interface IRenderMarkdownOptions {
17
+ color?: boolean;
18
+ codeBlockWidth?: number;
19
+ }
20
+
21
+ interface ITerminalRendererOptions {
22
+ code?: (text: string) => string;
23
+ }
24
+
25
+ interface IHighlightOptions {
26
+ ignoreIllegals?: boolean;
27
+ }
28
+
29
+ type TTerminalRendererConstructor = new (
30
+ options?: ITerminalRendererOptions,
31
+ highlightOptions?: IHighlightOptions,
32
+ ) => Renderer;
33
+
34
+ const TerminalRendererConstructor = TerminalRenderer as TTerminalRendererConstructor;
35
+
36
+ function shouldUseColor(option: boolean | undefined): boolean {
37
+ if (option !== undefined) {
38
+ return option;
39
+ }
40
+ if (process.env.NO_COLOR || process.env.FORCE_COLOR === ZERO_COLOR) {
41
+ return false;
42
+ }
43
+ if (process.env.FORCE_COLOR) {
44
+ return true;
45
+ }
46
+ return Boolean(process.stdout.isTTY);
47
+ }
48
+
49
+ function isDiffLanguage(language: string | undefined): boolean {
50
+ return language?.trim().toLowerCase() === 'diff';
51
+ }
52
+
53
+ function styleAddedOrRemovedDiffRow(line: string, rowWidth: number, color: boolean): string {
54
+ const row = `${CODE_BLOCK_INDENT}${line}`.padEnd(rowWidth);
55
+ if (!color) {
56
+ return row.trimEnd();
57
+ }
58
+ if (line.startsWith('+')) {
59
+ return `${ANSI_DARK_GREEN_BACKGROUND}${ANSI_LIGHT_GREEN}${row}${ANSI_RESET}`;
60
+ }
61
+ if (line.startsWith('-')) {
62
+ return `${ANSI_DARK_RED_BACKGROUND}${ANSI_LIGHT_RED}${row}${ANSI_RESET}`;
63
+ }
64
+ return row.trimEnd();
65
+ }
66
+
67
+ function colorizeDiffLine(line: string, color: boolean, rowWidth: number): string {
68
+ if (line.startsWith('+') || line.startsWith('-')) {
69
+ return styleAddedOrRemovedDiffRow(line, rowWidth, color);
70
+ }
71
+ const row = `${CODE_BLOCK_INDENT}${line}`;
72
+ if (!color) {
73
+ return row;
74
+ }
75
+ if (line.startsWith('@@')) {
76
+ return `${ANSI_CYAN}${row}${ANSI_RESET}`;
77
+ }
78
+ if (line.startsWith('diff ') || line.startsWith('index ')) {
79
+ return `${ANSI_DIM}${row}${ANSI_RESET}`;
80
+ }
81
+ return row;
82
+ }
83
+
84
+ function resolveDiffRowWidth(lines: readonly string[], requestedWidth: number | undefined): number {
85
+ const minimumWidth = lines.reduce(
86
+ (maxWidth, line) => Math.max(maxWidth, CODE_BLOCK_INDENT.length + line.length),
87
+ 0,
88
+ );
89
+ if (requestedWidth === undefined) {
90
+ return minimumWidth;
91
+ }
92
+ return Math.max(minimumWidth, requestedWidth);
93
+ }
94
+
95
+ function renderDiffCodeBlock(
96
+ code: string,
97
+ color: boolean,
98
+ codeBlockWidth: number | undefined,
99
+ ): string {
100
+ const lines = code.split('\n');
101
+ const rowWidth = resolveDiffRowWidth(lines, codeBlockWidth);
102
+ const body = lines.map((line) => colorizeDiffLine(line, color, rowWidth)).join('\n');
103
+ return `${body}\n\n`;
104
+ }
105
+
106
+ function createTerminalRenderer(color: boolean, codeBlockWidth: number | undefined): Renderer {
107
+ const renderer = new TerminalRendererConstructor(undefined, { ignoreIllegals: true });
108
+ const renderCode = renderer.code.bind(renderer);
109
+
110
+ renderer.code = (code: string, language: string | undefined, escaped: boolean): string => {
111
+ if (isDiffLanguage(language)) {
112
+ return renderDiffCodeBlock(code, color, codeBlockWidth);
113
+ }
114
+ return renderCode(code, language, escaped);
115
+ };
116
+
117
+ return renderer;
118
+ }
119
+
120
+ /**
121
+ * Render markdown to a terminal-formatted string with colors, bold, etc.
122
+ * Returns the rendered string (may include ANSI escape codes).
123
+ */
124
+ export function renderMarkdown(md: string, options: IRenderMarkdownOptions = {}): string {
125
+ const result = marked.parse(md, {
126
+ renderer: createTerminalRenderer(shouldUseColor(options.color), options.codeBlockWidth),
127
+ });
128
+ return typeof result === 'string' ? result.trimEnd() : md;
129
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Ink render entry point.
3
+ */
4
+
5
+ import React from 'react';
6
+ import { render } from 'ink';
7
+ import App from './App.js';
8
+ import type { IAIProvider } from '@robota-sdk/agent-core';
9
+ import type { TPermissionMode } from '@robota-sdk/agent-core';
10
+ import type {
11
+ IBackgroundTaskRunner,
12
+ ICommandHostAdapters,
13
+ ICommandModule,
14
+ IInteractiveSession,
15
+ IInteractiveSessionStore,
16
+ TSubagentRunnerFactory,
17
+ TShellExecFn,
18
+ CommandRegistry,
19
+ } from '@robota-sdk/agent-framework';
20
+ import type { ITransportRegistryView } from '@robota-sdk/agent-interface-transport';
21
+ import type { ITuiCliAdapter } from './tui-cli-adapter.js';
22
+
23
+ export interface IRenderOptions {
24
+ cwd: string;
25
+ provider: IAIProvider;
26
+ providerOverride?: string | undefined;
27
+ providerType?: string | undefined;
28
+ modelId?: string;
29
+ language?: string;
30
+ permissionMode?: TPermissionMode;
31
+ maxTurns?: number;
32
+ version?: string;
33
+ sessionStore?: IInteractiveSessionStore;
34
+ resumeSessionId?: string;
35
+ showSessionPickerOnStart?: boolean;
36
+ forkSession?: boolean;
37
+ sessionName?: string;
38
+ backgroundTaskRunners?: IBackgroundTaskRunner[];
39
+ subagentRunnerFactory?: TSubagentRunnerFactory;
40
+ commandModules?: readonly ICommandModule[];
41
+ commandHostAdapters?: ICommandHostAdapters;
42
+ shellExec?: TShellExecFn;
43
+ startupUpdateNotice?: Promise<string | undefined>;
44
+ transportRegistry?: ITransportRegistryView<IInteractiveSession>;
45
+ cliAdapter: ITuiCliAdapter;
46
+ reloadPluginCommandSource?: (registry: CommandRegistry) => void;
47
+ agentName?: string;
48
+ }
49
+
50
+ export async function renderApp(options: IRenderOptions): Promise<void> {
51
+ process.on('unhandledRejection', (reason) => {
52
+ process.stderr.write(`\n[UNHANDLED REJECTION] ${reason}\n`);
53
+ if (reason instanceof Error) {
54
+ process.stderr.write(`${reason.stack}\n`);
55
+ }
56
+ });
57
+
58
+ const instance = render(<App {...options} />, { exitOnCtrlC: false });
59
+ await instance.waitUntilExit();
60
+ }
@@ -0,0 +1,63 @@
1
+ export type TStatusActivityKind = 'tools' | 'thinking' | 'background' | 'queued' | 'idle';
2
+
3
+ export interface IStatusActivityInput {
4
+ isThinking: boolean;
5
+ activeToolCount: number;
6
+ activeBackgroundTaskCount: number;
7
+ hasPendingPrompt: boolean;
8
+ }
9
+
10
+ export interface IStatusActivity {
11
+ kind: TStatusActivityKind;
12
+ label: string;
13
+ color: string;
14
+ segments: string[];
15
+ text: string;
16
+ }
17
+
18
+ const NO_ACTIVE_ITEMS = 0;
19
+
20
+ export function formatStatusActivity(input: IStatusActivityInput): IStatusActivity {
21
+ const base = getPrimaryActivity(input);
22
+ const segments = input.hasPendingPrompt && base.kind !== 'queued' ? ['queued'] : [];
23
+ const text = [base.label, ...segments].join(' · ');
24
+ return { ...base, segments, text };
25
+ }
26
+
27
+ function getPrimaryActivity(
28
+ input: IStatusActivityInput,
29
+ ): Omit<IStatusActivity, 'segments' | 'text'> {
30
+ if (input.activeToolCount > NO_ACTIVE_ITEMS) {
31
+ return {
32
+ kind: 'tools',
33
+ label: `Tools x${input.activeToolCount}`,
34
+ color: 'cyan',
35
+ };
36
+ }
37
+ if (input.isThinking) {
38
+ return {
39
+ kind: 'thinking',
40
+ label: 'Thinking',
41
+ color: 'yellow',
42
+ };
43
+ }
44
+ if (input.activeBackgroundTaskCount > NO_ACTIVE_ITEMS) {
45
+ return {
46
+ kind: 'background',
47
+ label: `Background x${input.activeBackgroundTaskCount}`,
48
+ color: 'cyan',
49
+ };
50
+ }
51
+ if (input.hasPendingPrompt) {
52
+ return {
53
+ kind: 'queued',
54
+ label: 'Queued',
55
+ color: 'yellow',
56
+ };
57
+ }
58
+ return {
59
+ kind: 'idle',
60
+ label: 'Idle',
61
+ color: 'gray',
62
+ };
63
+ }
@@ -0,0 +1,12 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { ITuiCliAdapter } from './tui-cli-adapter.js';
3
+
4
+ const TuiCliAdapterContext = createContext<ITuiCliAdapter | null>(null);
5
+
6
+ export const TuiCliAdapterProvider = TuiCliAdapterContext.Provider;
7
+
8
+ export function useTuiCliAdapter(): ITuiCliAdapter {
9
+ const adapter = useContext(TuiCliAdapterContext);
10
+ if (!adapter) throw new Error('TuiCliAdapterContext not provided');
11
+ return adapter;
12
+ }
@@ -0,0 +1,25 @@
1
+ import type {
2
+ TStatusLineCommandSettingsPatch,
3
+ IStatusLineCommandSettings,
4
+ CommandRegistry,
5
+ } from '@robota-sdk/agent-framework';
6
+ import type { TUniversalValue } from '@robota-sdk/agent-core';
7
+
8
+ export interface ITuiCliAdapter {
9
+ getUserSettingsPath(): string;
10
+ readSettings(path: string): Record<string, TUniversalValue>;
11
+ writeSettings(path: string, settings: Record<string, TUniversalValue>): void;
12
+ deleteSettings(path: string): boolean;
13
+ applyStatusLineSettings(
14
+ path: string,
15
+ patch: TStatusLineCommandSettingsPatch,
16
+ ): IStatusLineCommandSettings;
17
+ reloadPluginCommandSource(registry: CommandRegistry): void;
18
+ applyActiveModelChange(
19
+ cwd: string,
20
+ modelId: string,
21
+ options?: { providerOverride?: string },
22
+ ): { applied: boolean };
23
+ getGitBranch(cwd: string): string | undefined;
24
+ getProviderDisplayName(type: string): string;
25
+ }