gentle-pi 2.5.0 → 2.6.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.
- package/README.md +139 -30
- package/assets/agents/gentle-ai-worker.md +4 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +21 -3
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/telemetry.md +57 -1
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +143 -15
- package/extensions/codegraph-tools.ts +1 -0
- package/extensions/gentle-agents.ts +799 -50
- package/extensions/gentle-ai.ts +2033 -322
- package/extensions/gentle-shell.ts +145 -42
- package/extensions/gentle-todo.ts +47 -12
- package/extensions/quiet-tools.ts +1 -0
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +7 -10
- package/lib/agents-history.ts +9 -1
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +77 -5
- package/lib/agents-runner.ts +548 -26
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +548 -191
- package/lib/agents-widget.ts +33 -14
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +8 -6
- package/lib/native-review-cli.ts +283 -1
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +296 -46
- package/lib/review-candidate-view.ts +30 -20
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +53 -8
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +187 -5
- package/lib/review-last-event-controller.ts +7 -4
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-session-standing-permission.ts +27 -6
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +47 -1
- package/lib/shell-card.ts +1 -4
- package/lib/shell-changes-view.ts +362 -37
- package/lib/shell-changes.ts +81 -1
- package/lib/shell-prompt.ts +11 -15
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +28 -11
- package/lib/telemetry-trigger.ts +2 -0
- package/package.json +6 -3
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +283 -1
- package/runtime/review-integration-v2.mjs +187 -5
- package/runtime/telemetry-trigger.mjs +2 -0
- package/scripts/build-runtime-modules.mjs +9 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +4 -2
- package/skills/_shared/review-ledger-contract.md +17 -1
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +62 -0
- package/tests/agents-fake-child.ts +15 -1
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +45 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner.test.ts +571 -14
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-view.test.ts +476 -65
- package/tests/agents-widget.test.ts +31 -1
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +169 -3
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +2 -1
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +2 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +1460 -33
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +38 -0
- package/tests/gentle-ai.test.ts +944 -4
- package/tests/gentle-shell.test.ts +303 -12
- package/tests/gentle-todo.test.ts +54 -10
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-review-capability-contract.test.ts +12 -1
- package/tests/native-review-cli.test.ts +277 -3
- package/tests/native-review-parity.test.ts +14 -7
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/package-manifest.test.ts +232 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +1 -0
- package/tests/rdd-aware-verification-contract.test.ts +10 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +304 -6
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +14 -0
- package/tests/review-controller-native-routing.test.ts +563 -3
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +364 -4
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +164 -0
- package/tests/review-last-event-closure.test.ts +105 -1
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-session-standing-permission-controller.test.ts +52 -4
- package/tests/review-session-standing-permission.test.ts +30 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-card.test.ts +24 -3
- package/tests/shell-changes-view.test.ts +471 -8
- package/tests/shell-changes.test.ts +168 -0
- package/tests/shell-prompt.test.ts +28 -6
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +100 -11
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +3 -1
package/lib/agents-view.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { Key, matchesKey, truncateToWidth, visibleWidth,
|
|
2
|
-
import {
|
|
3
|
-
import { isFinished, TASK_STATUS,
|
|
1
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
2
|
+
import { measureAgentsViewLayout, type AgentsViewLayout } from "./agents-view-layout.ts";
|
|
3
|
+
import { emptyThread, isFinished, TASK_STATUS, type TaskRecord, type TaskStore, type TaskThread, type ThreadItem } from "./agents-protocol.ts";
|
|
4
|
+
import { renderThreadItem, type AgentsThreadTheme } from "./agents-thread-view.ts";
|
|
4
5
|
import { formatElapsed } from "./agents-widget.ts";
|
|
6
|
+
import { createNativePointerScope, type NativePointerRegion } from "./native-pointer-region.ts";
|
|
5
7
|
import { formatTokens } from "./shell-bar.ts";
|
|
8
|
+
import { PresenceCursor, readActivity, type Header, type Target } from "./orchestrator-presence.ts";
|
|
6
9
|
|
|
7
10
|
// Gentle Agents overlay: tasks on the left, the selected task's thread on
|
|
8
11
|
// the right. Only the selected task is subscribed, thread items are rendered
|
|
9
12
|
// once each (they are immutable until replaced), and the viewport shows the
|
|
10
|
-
// tail unless the human scrolls up.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
+
// tail unless the human scrolls up. Current scope contains direct live children;
|
|
14
|
+
// all sessions is a presence directory, not a history browser. Remote activity
|
|
15
|
+
// is presentation-only and never enters the local TaskStore.
|
|
13
16
|
|
|
14
|
-
export interface AgentsViewTheme {
|
|
15
|
-
fg(color: string, text: string): string;
|
|
16
|
-
}
|
|
17
|
+
export interface AgentsViewTheme extends AgentsThreadTheme {}
|
|
17
18
|
|
|
18
19
|
export const VIEW_SCOPE = {
|
|
19
20
|
SESSION: "session",
|
|
@@ -24,14 +25,14 @@ export type ViewScope = (typeof VIEW_SCOPE)[keyof typeof VIEW_SCOPE];
|
|
|
24
25
|
|
|
25
26
|
export interface AgentsViewDeps {
|
|
26
27
|
theme: AgentsViewTheme;
|
|
27
|
-
rows: number;
|
|
28
|
+
rows: number | (() => number);
|
|
28
29
|
store: TaskStore;
|
|
29
|
-
// The active session; without it there is nothing to scope by and the
|
|
30
|
-
// list shows every task.
|
|
31
30
|
sessionId?: string;
|
|
31
|
+
presence?: { profile: string; target?: Readonly<Target> };
|
|
32
32
|
now(): number;
|
|
33
33
|
onCancel(task: TaskRecord): void;
|
|
34
34
|
canCancel?(task: TaskRecord): boolean;
|
|
35
|
+
isLocalTask?(task: TaskRecord): boolean;
|
|
35
36
|
onOpen(task: TaskRecord): void;
|
|
36
37
|
onClose(): void;
|
|
37
38
|
requestRender(): void;
|
|
@@ -41,16 +42,10 @@ const ROLE = {
|
|
|
41
42
|
FRAME: "border",
|
|
42
43
|
TITLE: "customMessageLabel",
|
|
43
44
|
SELECTED: "accent",
|
|
45
|
+
HOVER: "warning",
|
|
44
46
|
NAME: "text",
|
|
45
47
|
NAME_IDLE: "muted",
|
|
46
48
|
META: "dim",
|
|
47
|
-
TEXT: "text",
|
|
48
|
-
THINKING: "dim",
|
|
49
|
-
TOOL: "accent",
|
|
50
|
-
TOOL_ERROR: "error",
|
|
51
|
-
OUTPUT: "muted",
|
|
52
|
-
NOTE: "muted",
|
|
53
|
-
NOTE_ERROR: "warning",
|
|
54
49
|
KEY: "accent",
|
|
55
50
|
KEY_TEXT: "dim",
|
|
56
51
|
EMPTY: "dim",
|
|
@@ -74,10 +69,6 @@ const GLYPH_ROLE: Record<string, string> = {
|
|
|
74
69
|
[TASK_STATUS.CANCELLED]: "dim",
|
|
75
70
|
[TASK_STATUS.TIMED_OUT]: "error",
|
|
76
71
|
};
|
|
77
|
-
const LIST_MAX_WIDTH = 34;
|
|
78
|
-
const LIST_RATIO = 0.32;
|
|
79
|
-
const CHROME_ROWS = 3;
|
|
80
|
-
const MIN_BODY_ROWS = 1;
|
|
81
72
|
export const SESSION_FINISHED_TTL_MS = 15 * 60_000;
|
|
82
73
|
const SCOPE_LABEL: Record<ViewScope, string> = { [VIEW_SCOPE.SESSION]: "this session", [VIEW_SCOPE.ALL]: "all sessions" };
|
|
83
74
|
const SCOPE_KEY: Record<ViewScope, string> = { [VIEW_SCOPE.SESSION]: "all sessions", [VIEW_SCOPE.ALL]: "this session" };
|
|
@@ -85,13 +76,17 @@ const EMPTY_LIST = "no tasks yet";
|
|
|
85
76
|
const EMPTY_THREAD = "waiting for the first event";
|
|
86
77
|
const FOLLOW_BUTTON = "[ Follow ]";
|
|
87
78
|
const OPEN_BUTTON = "[ Open session ]";
|
|
88
|
-
const
|
|
79
|
+
const CLOSE_BUTTON = "[× Close]";
|
|
80
|
+
// At the 60-cell split boundary these four controls fit before any hints.
|
|
81
|
+
const STOP_BUTTON = "[Stop]";
|
|
82
|
+
const SCOPE_BUTTON = "[Scope]";
|
|
89
83
|
const KEYS = [
|
|
90
84
|
["j/k", "task"],
|
|
91
85
|
["ctrl+j/k", "scroll"],
|
|
92
86
|
["f", "follow"],
|
|
93
87
|
["o", "open session"],
|
|
94
|
-
["esc", "
|
|
88
|
+
["esc", "back"],
|
|
89
|
+
["q", "close"],
|
|
95
90
|
] as const;
|
|
96
91
|
|
|
97
92
|
const EMPTY_COMPONENT: Component = {
|
|
@@ -104,19 +99,25 @@ interface PointerButtonLayout {
|
|
|
104
99
|
width: number;
|
|
105
100
|
}
|
|
106
101
|
|
|
107
|
-
interface PointerLayout {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
threadWidth: number;
|
|
114
|
-
bodyRows: number;
|
|
115
|
-
footerY: number;
|
|
116
|
-
followButton?: PointerButtonLayout;
|
|
117
|
-
openButton?: PointerButtonLayout;
|
|
102
|
+
interface PointerLayout extends AgentsViewLayout {
|
|
103
|
+
narrowView: "list" | "details";
|
|
104
|
+
sourceHeight?: number;
|
|
105
|
+
closeButton?: PointerButtonLayout;
|
|
106
|
+
modeButton?: PointerButtonLayout;
|
|
107
|
+
actions?: Array<PointerButtonLayout & { region: NativePointerRegion }>;
|
|
118
108
|
}
|
|
119
109
|
|
|
110
|
+
interface SessionGroup {
|
|
111
|
+
id: string;
|
|
112
|
+
sessionId: string | undefined;
|
|
113
|
+
label?: string;
|
|
114
|
+
tasks: TaskRecord[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type VisibleRow =
|
|
118
|
+
| { id: string; kind: "heading"; group: SessionGroup }
|
|
119
|
+
| { id: string; kind: "task"; group: SessionGroup; task: TaskRecord };
|
|
120
|
+
|
|
120
121
|
function rule(length: number): string {
|
|
121
122
|
return "─".repeat(Math.max(0, length));
|
|
122
123
|
}
|
|
@@ -126,49 +127,6 @@ function fit(text: string, width: number): string {
|
|
|
126
127
|
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
function argsSummary(item: ToolItem): string {
|
|
130
|
-
const values = Object.values(item.args).filter((value) => typeof value === "string") as string[];
|
|
131
|
-
return (values[0] ?? "").replace(/\s+/g, " ").trim();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function toolLines(item: ToolItem, theme: AgentsViewTheme, width: number): string[] {
|
|
135
|
-
const role = item.isError ? ROLE.TOOL_ERROR : ROLE.TOOL;
|
|
136
|
-
const head = truncateToWidth(`▸ ${item.name} ${argsSummary(item)}`, width, "…");
|
|
137
|
-
const lines = [theme.fg(role, head)];
|
|
138
|
-
// The whole captured output, wrapped: the thread pane scrolls, so nothing
|
|
139
|
-
// is hidden here. The store already keeps only the last maxOutputChars of a
|
|
140
|
-
// tool's output and marks the cut with a leading ellipsis.
|
|
141
|
-
for (const line of item.output.split("\n").filter((line) => line.length > 0)) {
|
|
142
|
-
for (const wrapped of wrapTextWithAnsi(line, Math.max(1, width - 2))) lines.push(theme.fg(ROLE.OUTPUT, ` ${wrapped}`));
|
|
143
|
-
}
|
|
144
|
-
if (item.running) lines.push(theme.fg(ROLE.META, " …"));
|
|
145
|
-
return lines;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function thinkingLines(text: string, theme: AgentsViewTheme, width: number): string[] {
|
|
149
|
-
const lines: string[] = [];
|
|
150
|
-
for (const [index, line] of text.split("\n").filter((line) => line.length > 0).entries()) {
|
|
151
|
-
const prefix = index === 0 ? "∴ " : " ";
|
|
152
|
-
for (const wrapped of wrapTextWithAnsi(line, Math.max(1, width - 2))) lines.push(theme.fg(ROLE.THINKING, `${prefix}${wrapped}`));
|
|
153
|
-
}
|
|
154
|
-
return lines;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function itemLines(item: ThreadItem, theme: AgentsViewTheme, width: number): string[] {
|
|
158
|
-
switch (item.kind) {
|
|
159
|
-
case THREAD_ITEM.TEXT:
|
|
160
|
-
return wrapTextWithAnsi(item.text, width).map((line) => theme.fg(ROLE.TEXT, line));
|
|
161
|
-
case THREAD_ITEM.THINKING:
|
|
162
|
-
return thinkingLines(item.text, theme, width);
|
|
163
|
-
case THREAD_ITEM.TOOL:
|
|
164
|
-
return toolLines(item, theme, width);
|
|
165
|
-
case THREAD_ITEM.NOTE:
|
|
166
|
-
return [theme.fg(item.text.startsWith("error") ? ROLE.NOTE_ERROR : ROLE.NOTE, truncateToWidth(`· ${item.text}`, width, "…"))];
|
|
167
|
-
default:
|
|
168
|
-
return [];
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
130
|
export function taskHeader(task: TaskRecord, now: number): string {
|
|
173
131
|
const parts = [task.agent, task.status, task.model, task.tokens > 0 ? formatTokens(task.tokens) : "", task.cost > 0 ? `$${task.cost.toFixed(2)}` : "", task.startedAt === null ? "" : formatElapsed((task.endedAt ?? now) - task.startedAt)];
|
|
174
132
|
return parts.filter((part) => part.length > 0).join(" · ");
|
|
@@ -177,9 +135,17 @@ export function taskHeader(task: TaskRecord, now: number): string {
|
|
|
177
135
|
export class AgentsView {
|
|
178
136
|
private readonly deps: AgentsViewDeps;
|
|
179
137
|
private tasks: TaskRecord[] = [];
|
|
180
|
-
private
|
|
181
|
-
private
|
|
138
|
+
private selectedId: string | undefined;
|
|
139
|
+
private hoveredId: string | undefined;
|
|
140
|
+
private narrowView: "list" | "details" = "list";
|
|
141
|
+
private narrowGroup: string | undefined;
|
|
142
|
+
private groupCursor: string | undefined;
|
|
143
|
+
private footerPage = 0;
|
|
144
|
+
private readonly actionRegions = new Map<string, NativePointerRegion>();
|
|
145
|
+
private readonly expanded = new Map<string, boolean>();
|
|
146
|
+
private readonly observedActiveGroups = new Set<string>();
|
|
182
147
|
private listScroll = 0;
|
|
148
|
+
private readonly manualListOffsets = new Map<string, number>();
|
|
183
149
|
private scope: ViewScope;
|
|
184
150
|
private scroll = 0;
|
|
185
151
|
private follow = true;
|
|
@@ -189,16 +155,95 @@ export class AgentsView {
|
|
|
189
155
|
private readonly threadRegion: NativePointerRegion;
|
|
190
156
|
private readonly followRegion: NativePointerRegion;
|
|
191
157
|
private readonly openRegion: NativePointerRegion;
|
|
192
|
-
private
|
|
158
|
+
private readonly closeRegion: NativePointerRegion;
|
|
159
|
+
private readonly modeRegion: NativePointerRegion;
|
|
160
|
+
private hoveredControl: "follow" | "open" | "close" | "mode" | undefined;
|
|
193
161
|
private pointerLayout: PointerLayout | undefined;
|
|
162
|
+
// Keyboard input has no terminal width, so retain its last rendered width
|
|
163
|
+
// independently from disposable pointer hit geometry.
|
|
164
|
+
private lastRenderedWidth: number | undefined;
|
|
165
|
+
private closed = false;
|
|
194
166
|
private unsubscribeTask: (() => void) | undefined;
|
|
167
|
+
private subscribedTaskId: string | undefined;
|
|
195
168
|
private readonly unsubscribeSummary: () => void;
|
|
196
169
|
private cache = new WeakMap<ThreadItem, string[]>();
|
|
197
170
|
private cacheWidth = -1;
|
|
171
|
+
private peers: SessionGroup[] = [];
|
|
172
|
+
private remoteThreads = new Map<string, TaskThread>();
|
|
173
|
+
private presenceCursor?: PresenceCursor;
|
|
174
|
+
private presenceTimer?: ReturnType<typeof setTimeout>;
|
|
175
|
+
|
|
176
|
+
// One directory page or one pinned activity per turn; yield between reads.
|
|
177
|
+
// Keep the previous directory until a traversal completes, avoiding page flicker.
|
|
178
|
+
private refreshPresence(): void {
|
|
179
|
+
const source = this.deps.presence;
|
|
180
|
+
if (!source || this.closed) return;
|
|
181
|
+
const groups: SessionGroup[] = [];
|
|
182
|
+
const threads = new Map<string, TaskThread>();
|
|
183
|
+
const seen = new Set<string>();
|
|
184
|
+
let pending: Header[] = [];
|
|
185
|
+
let overflow = true;
|
|
186
|
+
const step = () => {
|
|
187
|
+
if (this.closed) return;
|
|
188
|
+
try {
|
|
189
|
+
if (pending.length) {
|
|
190
|
+
const header = pending.shift()!;
|
|
191
|
+
const id = `${header.sessionHash}:${header.incarnation}`;
|
|
192
|
+
if (!seen.has(id) && header.incarnation !== source.target?.incarnation) {
|
|
193
|
+
seen.add(id);
|
|
194
|
+
const { activity, unavailable } = readActivity(source.profile, header);
|
|
195
|
+
const tasks = (activity?.tasks ?? []).filter(({ summary }) => !isFinished(summary.status as TaskRecord["status"])).map(({ summary, thread }) => {
|
|
196
|
+
const task: TaskRecord = { ...summary, id: `peer:${id}:${summary.id}`, parentSessionId: id,
|
|
197
|
+
status: summary.status as TaskRecord["status"], mode: "background", prompt: "", cwd: "",
|
|
198
|
+
thinking: undefined, sessionPath: null, error: null, result: null, lastStep: "", turns: 0, toolCalls: 0, tokens: 0, cost: 0 };
|
|
199
|
+
threads.set(task.id, { ...emptyThread(), ...thread,
|
|
200
|
+
items: thread.items.map((item) => item.kind === "tool" ? { ...item, args: {} } : item) as ThreadItem[] });
|
|
201
|
+
return task;
|
|
202
|
+
});
|
|
203
|
+
groups.push({ id, sessionId: id, label: `${header.label}${unavailable ? " · unavailable" : ""}`, tasks });
|
|
204
|
+
}
|
|
205
|
+
} else if (overflow) {
|
|
206
|
+
this.presenceCursor ??= new PresenceCursor(source.profile);
|
|
207
|
+
const page = this.presenceCursor.next();
|
|
208
|
+
pending = page.entries.filter((entry) => entry.recent);
|
|
209
|
+
overflow = page.overflow;
|
|
210
|
+
} else {
|
|
211
|
+
this.peers = groups;
|
|
212
|
+
this.remoteThreads = threads;
|
|
213
|
+
this.refreshTasks();
|
|
214
|
+
this.clearFooterLayout();
|
|
215
|
+
this.deps.requestRender();
|
|
216
|
+
this.stopPresenceRefresh();
|
|
217
|
+
this.presenceTimer = setTimeout(() => this.refreshPresence(), 1000);
|
|
218
|
+
this.presenceTimer.unref();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
this.peers = [];
|
|
223
|
+
this.remoteThreads.clear();
|
|
224
|
+
this.refreshTasks();
|
|
225
|
+
this.clearFooterLayout();
|
|
226
|
+
this.deps.requestRender();
|
|
227
|
+
this.stopPresenceRefresh();
|
|
228
|
+
this.presenceTimer = setTimeout(() => this.refreshPresence(), 1000);
|
|
229
|
+
this.presenceTimer.unref();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.presenceTimer = setTimeout(step, 0);
|
|
233
|
+
this.presenceTimer.unref();
|
|
234
|
+
};
|
|
235
|
+
step();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private stopPresenceRefresh(): void {
|
|
239
|
+
clearTimeout(this.presenceTimer);
|
|
240
|
+
this.presenceCursor?.close();
|
|
241
|
+
this.presenceCursor = undefined;
|
|
242
|
+
}
|
|
198
243
|
|
|
199
244
|
constructor(deps: AgentsViewDeps) {
|
|
200
245
|
this.deps = deps;
|
|
201
|
-
this.scope =
|
|
246
|
+
this.scope = VIEW_SCOPE.SESSION;
|
|
202
247
|
this.listRegion = this.pointerScope.wrap(EMPTY_COMPONENT, {
|
|
203
248
|
onWheel: (event) => this.wheelList(event),
|
|
204
249
|
});
|
|
@@ -215,6 +260,16 @@ export class AgentsView {
|
|
|
215
260
|
onLeave: () => this.clearHoveredControl("open"),
|
|
216
261
|
onClick: (event) => this.clickOpen(event),
|
|
217
262
|
});
|
|
263
|
+
this.closeRegion = this.pointerScope.wrap(EMPTY_COMPONENT, {
|
|
264
|
+
onHover: () => this.hoverControl("close"),
|
|
265
|
+
onLeave: () => this.clearHoveredControl("close"),
|
|
266
|
+
onClick: (event) => this.clickClose(event),
|
|
267
|
+
});
|
|
268
|
+
this.modeRegion = this.pointerScope.wrap(EMPTY_COMPONENT, {
|
|
269
|
+
onHover: () => this.hoverControl("mode"),
|
|
270
|
+
onLeave: () => this.clearHoveredControl("mode"),
|
|
271
|
+
onClick: (event) => this.clickMode(event),
|
|
272
|
+
});
|
|
218
273
|
this.refreshTasks();
|
|
219
274
|
this.unsubscribeSummary = deps.store.subscribeSummary(() => {
|
|
220
275
|
this.clearFooterLayout();
|
|
@@ -222,12 +277,17 @@ export class AgentsView {
|
|
|
222
277
|
this.deps.requestRender();
|
|
223
278
|
});
|
|
224
279
|
this.subscribeSelected();
|
|
280
|
+
this.refreshPresence();
|
|
225
281
|
}
|
|
226
282
|
|
|
227
283
|
dispose(): void {
|
|
284
|
+
this.closed = true;
|
|
285
|
+
this.stopPresenceRefresh();
|
|
228
286
|
this.pointerLayout = undefined;
|
|
229
287
|
this.pointerScope.dispose();
|
|
230
288
|
this.unsubscribeTask?.();
|
|
289
|
+
this.unsubscribeTask = undefined;
|
|
290
|
+
this.subscribedTaskId = undefined;
|
|
231
291
|
this.unsubscribeSummary();
|
|
232
292
|
}
|
|
233
293
|
|
|
@@ -236,73 +296,127 @@ export class AgentsView {
|
|
|
236
296
|
}
|
|
237
297
|
|
|
238
298
|
selectedTask(): TaskRecord | undefined {
|
|
239
|
-
return this.tasks
|
|
299
|
+
return this.tasks.find((task) => `task:${task.id}` === this.selectedId);
|
|
240
300
|
}
|
|
241
301
|
|
|
242
302
|
handleInput(data: string): void {
|
|
243
|
-
if (
|
|
244
|
-
|
|
303
|
+
if (this.closed) return;
|
|
304
|
+
if (data === "q") return this.close();
|
|
305
|
+
if (matchesKey(data, Key.escape)) return this.back();
|
|
306
|
+
if (data === "F" && !this.isNarrow()) {
|
|
307
|
+
this.setNarrowView(this.narrowView === "list" ? "details" : "list");
|
|
245
308
|
return;
|
|
246
309
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
310
|
+
if ((data === "\t" || matchesKey(data, Key.enter)) && this.isNarrow()) {
|
|
311
|
+
if (this.narrowView === "details") this.back();
|
|
312
|
+
else this.activate();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const rows = this.visibleRows();
|
|
316
|
+
const selected = this.selectedRow(rows);
|
|
317
|
+
const task = this.actionableTask();
|
|
318
|
+
const index = selected ? rows.findIndex((row) => row.id === selected.id) : -1;
|
|
319
|
+
if (this.narrowView === "details" && (data === "j" || data === "k" || matchesKey(data, Key.down) || matchesKey(data, Key.up))) {
|
|
320
|
+
this.scrollBy(data === "j" || matchesKey(data, Key.down) ? 1 : -1);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (data === "j" || matchesKey(data, Key.down)) this.select(index + 1, rows);
|
|
324
|
+
else if (data === "k" || matchesKey(data, Key.up)) this.select(index - 1, rows);
|
|
325
|
+
else if (matchesKey(data, Key.left) && selected?.kind === "heading") this.setExpanded(selected.group, false);
|
|
326
|
+
else if (matchesKey(data, Key.right) && selected?.kind === "heading") this.setExpanded(selected.group, true);
|
|
250
327
|
else if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("j"))) this.scrollBy(this.pageRows());
|
|
251
328
|
else if (matchesKey(data, Key.pageUp) || matchesKey(data, Key.ctrl("k"))) this.scrollBy(-this.pageRows());
|
|
252
|
-
else if (data === "f") {
|
|
329
|
+
else if (data === "f" && task) {
|
|
253
330
|
this.follow = true;
|
|
254
331
|
this.deps.requestRender();
|
|
255
332
|
} else if (data === "a" && this.deps.sessionId !== undefined) this.toggleScope();
|
|
256
333
|
else if ((data === "s" || data === "c") && task && this.canCancel(task)) this.deps.onCancel(task);
|
|
257
|
-
else if ((data === "o" || matchesKey(data, Key.enter)) && this.canOpen(task)) this.deps.onOpen(task
|
|
334
|
+
else if ((data === "o" || matchesKey(data, Key.enter)) && task && this.canOpen(task)) this.deps.onOpen(task);
|
|
258
335
|
}
|
|
259
336
|
|
|
260
337
|
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
338
|
+
if (this.closed) return undefined;
|
|
339
|
+
const live = this.layout(event.width);
|
|
340
|
+
const liveHeight = live.mode === "fallback" ? Math.min(1, live.height) : live.height;
|
|
341
|
+
if (event.width !== live.width || event.height !== liveHeight) return undefined;
|
|
261
342
|
const layout = this.pointerLayout;
|
|
262
|
-
if (!layout || event.width !== layout.width || event.height !== layout.height || event.x < 0 || event.y < 0 || event.x >= layout.width || event.y >= layout.height) return undefined;
|
|
343
|
+
if (!layout || (layout.sourceHeight !== undefined && layout.sourceHeight !== live.height) || event.width !== layout.width || event.height !== layout.height || live.mode !== layout.mode || layout.narrowView !== this.narrowView || event.x < 0 || event.y < 0 || event.x >= layout.width || event.y >= layout.height) return undefined;
|
|
344
|
+
if (event.y === 0) {
|
|
345
|
+
if (this.isInButton(event.x, layout.modeButton)) return this.modeRegion.handleMouse(event);
|
|
346
|
+
if (this.isInButton(event.x, layout.closeButton)) return this.closeRegion.handleMouse(event);
|
|
347
|
+
}
|
|
263
348
|
if (event.y >= 1 && event.y < 1 + layout.bodyRows) {
|
|
264
349
|
const row = event.y - 1;
|
|
265
|
-
if (event.x >= layout.listX && event.x < layout.listX + layout.listWidth) {
|
|
266
|
-
if (event.type === "wheel") return this.listRegion.handleMouse(event);
|
|
267
|
-
const
|
|
268
|
-
return
|
|
350
|
+
if (layout.mode === "narrow" && event.x >= layout.listX && event.x < layout.listX + layout.listWidth) {
|
|
351
|
+
if (event.type === "wheel") return this.narrowView === "list" ? this.listRegion.handleMouse(event) : this.threadRegion.handleMouse(event);
|
|
352
|
+
const visible = this.narrowView === "list" ? this.visibleRows()[this.listScroll + row] : undefined;
|
|
353
|
+
return visible ? this.taskRegion(row).handleMouse(event) : undefined;
|
|
269
354
|
}
|
|
270
|
-
if (event.x >= layout.
|
|
271
|
-
return this.
|
|
355
|
+
if (layout.mode === "panes" && event.x >= layout.listX && event.x < layout.listX + layout.listWidth) {
|
|
356
|
+
if (event.type === "wheel") return this.listRegion.handleMouse(event);
|
|
357
|
+
const visible = this.visibleRows()[this.listScroll + row];
|
|
358
|
+
return visible ? this.taskRegion(row).handleMouse(event) : undefined;
|
|
272
359
|
}
|
|
360
|
+
if (layout.mode === "panes" && event.x >= layout.threadX && event.x < layout.threadX + layout.threadWidth && event.type === "wheel") return this.threadRegion.handleMouse(event);
|
|
273
361
|
}
|
|
274
362
|
if (event.y === layout.footerY) {
|
|
275
|
-
if (this.isInButton(event.x,
|
|
276
|
-
if (this.isInButton(event.x, layout.openButton)) return this.openRegion.handleMouse(event);
|
|
363
|
+
for (const action of layout.actions ?? []) if (this.isInButton(event.x, action)) return action.region.handleMouse(event);
|
|
277
364
|
}
|
|
278
365
|
return undefined;
|
|
279
366
|
}
|
|
280
367
|
|
|
281
368
|
render(width: number): string[] {
|
|
282
|
-
|
|
283
|
-
|
|
369
|
+
const layout = this.layout(width);
|
|
370
|
+
this.lastRenderedWidth = layout.width;
|
|
371
|
+
if (layout.width === 0 || layout.height === 0) {
|
|
372
|
+
this.clearFooterLayout();
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
if (layout.mode === "fallback") {
|
|
376
|
+
this.clearFooterLayout();
|
|
377
|
+
this.closeRegion.setDisabled(false);
|
|
378
|
+
this.closeRegion.render(1);
|
|
379
|
+
this.pointerLayout = { ...layout, height: 1, sourceHeight: layout.height, narrowView: this.narrowView, closeButton: { x: 0, width: 1 } };
|
|
380
|
+
return ["×" + fit(" Close Agents", layout.width - 1)];
|
|
381
|
+
}
|
|
382
|
+
// Retained terminal rows never belong in either live scope.
|
|
284
383
|
const now = this.deps.now();
|
|
285
384
|
if (this.tasks.some((task) => !this.inScope(task, now))) this.refreshTasks();
|
|
286
|
-
if (this.pointerLayout && this.pointerLayout.width !== width) this.clearFooterLayout();
|
|
385
|
+
if (this.pointerLayout && (this.pointerLayout.width !== layout.width || this.pointerLayout.height !== layout.height || this.pointerLayout.mode !== layout.mode || this.pointerLayout.narrowView !== this.narrowView)) this.clearFooterLayout();
|
|
287
386
|
const theme = this.deps.theme;
|
|
288
|
-
const inner = width - 2;
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
this.followSelection(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
387
|
+
const inner = layout.width - 2;
|
|
388
|
+
const closeLabel = this.closeLabel(layout);
|
|
389
|
+
const modeLabel = this.modeLabel(layout);
|
|
390
|
+
this.closeRegion.setDisabled(closeLabel === undefined);
|
|
391
|
+
this.modeRegion.setDisabled(modeLabel === undefined);
|
|
392
|
+
this.followSelection(layout.bodyRows);
|
|
393
|
+
const closeWidth = visibleWidth(closeLabel ?? "");
|
|
394
|
+
const modeWidth = visibleWidth(modeLabel ?? "");
|
|
395
|
+
const closeX = layout.width - 1 - closeWidth;
|
|
396
|
+
this.pointerLayout = {
|
|
397
|
+
...layout,
|
|
398
|
+
narrowView: this.narrowView,
|
|
399
|
+
closeButton: closeLabel ? { x: closeX, width: closeWidth } : undefined,
|
|
400
|
+
modeButton: modeLabel ? { x: closeX - modeWidth - 1, width: modeWidth } : undefined,
|
|
401
|
+
};
|
|
402
|
+
this.listRegion.render(layout.listWidth);
|
|
403
|
+
this.threadRegion.render(layout.threadWidth);
|
|
404
|
+
if (closeLabel) this.closeRegion.render(closeWidth);
|
|
405
|
+
if (modeLabel) this.modeRegion.render(modeWidth);
|
|
297
406
|
const scope = this.deps.sessionId === undefined ? "" : `${SCOPE_LABEL[this.scope]} · `;
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
const
|
|
407
|
+
const controlsWidth = (closeLabel ? closeWidth + 1 : 0) + (modeLabel ? modeWidth + 1 : 0);
|
|
408
|
+
const title = truncateToWidth(`❀ Agents · ${scope}${this.counts()}`, Math.max(0, inner - 3 - controlsWidth), "…");
|
|
409
|
+
const mode = modeLabel ? ` ${theme.fg(this.hoveredControl === "mode" ? "warning" : ROLE.KEY, modeLabel)}` : "";
|
|
410
|
+
const close = closeLabel ? ` ${theme.fg(this.hoveredControl === "close" ? "warning" : ROLE.KEY, closeLabel)}` : "";
|
|
411
|
+
const top = theme.fg(ROLE.FRAME, "╭─ ") + theme.fg(ROLE.TITLE, title) + theme.fg(ROLE.FRAME, ` ${rule(inner - visibleWidth(title) - 3 - controlsWidth)}`) + mode + close + theme.fg(ROLE.FRAME, "╮");
|
|
412
|
+
const detail = layout.mode === "panes" || this.narrowView === "details" ? this.threadWindow(layout.bodyRows, layout.threadWidth) : [];
|
|
301
413
|
const body: string[] = [];
|
|
302
|
-
for (let row = 0; row <
|
|
303
|
-
body.push(`${theme.fg(ROLE.FRAME, "│")} ${fit(this.taskLine(row), listWidth)} ${theme.fg(ROLE.FRAME, "│")} ${fit(
|
|
414
|
+
for (let row = 0; row < layout.bodyRows; row += 1) {
|
|
415
|
+
if (layout.mode === "panes") body.push(`${theme.fg(ROLE.FRAME, "│")} ${fit(this.taskLine(row), layout.listWidth)} ${theme.fg(ROLE.FRAME, "│")} ${fit(detail[row] ?? "", layout.threadWidth)}${theme.fg(ROLE.FRAME, "│")}`);
|
|
416
|
+
else body.push(`${theme.fg(ROLE.FRAME, "│")} ${fit(this.narrowView === "list" ? this.taskLine(row) : detail[row] ?? "", layout.listWidth)} ${theme.fg(ROLE.FRAME, "│")}`);
|
|
304
417
|
}
|
|
305
|
-
const
|
|
418
|
+
const keyHints = this.selectedRow()?.kind === "heading" ? [["←/→", "group"] as const, ...this.keys(layout.mode === "narrow")] : this.keys(layout.mode === "narrow");
|
|
419
|
+
const keys = keyHints.map(([key, label]) => `${theme.fg(ROLE.KEY, key)} ${theme.fg(ROLE.KEY_TEXT, label)}`).join(" ");
|
|
306
420
|
const footer = this.footer(keys, inner - 2);
|
|
307
421
|
const keysLine = `${theme.fg(ROLE.FRAME, "│")} ${fit(footer, inner - 2)} ${theme.fg(ROLE.FRAME, "│")}`;
|
|
308
422
|
return [top, ...body, keysLine, theme.fg(ROLE.FRAME, `╰${rule(inner)}╯`)];
|
|
@@ -314,91 +428,241 @@ export class AgentsView {
|
|
|
314
428
|
|
|
315
429
|
private counts(): string {
|
|
316
430
|
const active = this.tasks.filter((task) => !isFinished(task.status)).length;
|
|
317
|
-
return `${active} active
|
|
431
|
+
return `${active} active`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private actionableTask(): TaskRecord | undefined {
|
|
435
|
+
return this.directoryRoot() && this.narrowView === "list" ? undefined : this.selectedTask();
|
|
318
436
|
}
|
|
319
437
|
|
|
320
438
|
private canCancel(task: TaskRecord): boolean {
|
|
321
|
-
return !isFinished(task.status) && (this.deps.canCancel?.(task) ?? true);
|
|
439
|
+
return !this.remoteThreads.has(task.id) && !isFinished(task.status) && (this.deps.canCancel?.(task) ?? true);
|
|
322
440
|
}
|
|
323
441
|
|
|
324
442
|
private canOpen(task: TaskRecord | undefined): boolean {
|
|
325
|
-
return Boolean(task?.sessionPath);
|
|
443
|
+
return Boolean(task?.sessionPath) && !this.remoteThreads.has(task!.id);
|
|
326
444
|
}
|
|
327
445
|
|
|
328
|
-
private keys(): ReadonlyArray<readonly [string, string]> {
|
|
329
|
-
const task = this.
|
|
446
|
+
private keys(narrow = false): ReadonlyArray<readonly [string, string]> {
|
|
447
|
+
const task = this.actionableTask();
|
|
448
|
+
const details = narrow && task ? [["tab", this.narrowView === "list" ? "details" : "back"]] as const : [];
|
|
330
449
|
const stop = task && this.canCancel(task) ? [["s", "Stop selected"]] as const : [];
|
|
331
450
|
const scope = this.deps.sessionId === undefined ? [] : [["a", SCOPE_KEY[this.scope]]] as const;
|
|
332
|
-
return [...KEYS.slice(0, 3), ...stop, ...scope, ...KEYS.slice(3)];
|
|
451
|
+
return [...KEYS.slice(0, 1), ...details, ...KEYS.slice(1, 3), ...stop, ...scope, ...KEYS.slice(3)];
|
|
333
452
|
}
|
|
334
453
|
|
|
335
454
|
// A new scope reads from the top: selection, list window, and thread reset.
|
|
336
455
|
private toggleScope(): void {
|
|
337
456
|
this.clearFooterLayout();
|
|
338
457
|
this.scope = this.scope === VIEW_SCOPE.SESSION ? VIEW_SCOPE.ALL : VIEW_SCOPE.SESSION;
|
|
458
|
+
this.narrowGroup = undefined;
|
|
459
|
+
this.groupCursor = undefined;
|
|
460
|
+
this.manualListOffsets.clear();
|
|
461
|
+
this.narrowView = "list";
|
|
339
462
|
this.tasks = [];
|
|
340
|
-
this.
|
|
463
|
+
this.selectedId = undefined;
|
|
341
464
|
this.listScroll = 0;
|
|
342
|
-
this.
|
|
465
|
+
this.hoveredId = undefined;
|
|
343
466
|
this.scroll = 0;
|
|
344
467
|
this.follow = true;
|
|
345
468
|
this.refreshTasks();
|
|
346
469
|
this.deps.requestRender();
|
|
347
470
|
}
|
|
348
471
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
472
|
+
private inScope(task: TaskRecord, _now: number): boolean {
|
|
473
|
+
return !isFinished(task.status) && (task.parentSessionId === this.deps.sessionId
|
|
474
|
+
|| (this.scope === VIEW_SCOPE.ALL && this.remoteThreads.has(task.id)));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private directoryRoot(): boolean {
|
|
478
|
+
return this.scope === VIEW_SCOPE.ALL && this.isNarrow() && !this.narrowGroup;
|
|
355
479
|
}
|
|
356
480
|
|
|
357
481
|
// Keep the selected row inside the list window, moving the window by the
|
|
358
482
|
// least amount needed; the wheel moves the same window on its own.
|
|
359
483
|
private followSelection(rows: number): void {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
484
|
+
const manual = this.manualListOffsets.get(this.listKey());
|
|
485
|
+
if (manual !== undefined) {
|
|
486
|
+
this.listScroll = Math.min(manual, Math.max(0, this.visibleRows().length - rows));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const selected = this.selectedRow();
|
|
490
|
+
const index = selected ? this.visibleRows().findIndex((row) => row.id === selected.id) : -1;
|
|
491
|
+
if (index >= 0 && index < this.listScroll) this.listScroll = index;
|
|
492
|
+
else if (index >= this.listScroll + rows) this.listScroll = index - rows + 1;
|
|
493
|
+
this.listScroll = Math.max(0, Math.min(this.listScroll, Math.max(0, this.visibleRows().length - rows)));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private layout(width = this.pointerLayout?.width ?? this.lastRenderedWidth ?? 80): AgentsViewLayout {
|
|
497
|
+
const rows = typeof this.deps.rows === "function" ? this.deps.rows() : this.deps.rows;
|
|
498
|
+
return measureAgentsViewLayout(width, rows, this.narrowView === "details");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private listKey(): string {
|
|
502
|
+
return this.isNarrow() ? this.narrowGroup ?? "root" : "panes";
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private isNarrow(): boolean {
|
|
506
|
+
return measureAgentsViewLayout(this.lastRenderedWidth ?? 80, 3).mode !== "panes";
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private back(): void {
|
|
510
|
+
if (this.narrowView === "details") this.setNarrowView("list");
|
|
511
|
+
else if (this.isNarrow() && this.narrowGroup) {
|
|
512
|
+
this.groupCursor = `heading:${this.narrowGroup}`;
|
|
513
|
+
this.narrowGroup = undefined;
|
|
514
|
+
this.listScroll = 0;
|
|
515
|
+
this.clearFooterLayout();
|
|
516
|
+
this.deps.requestRender();
|
|
517
|
+
} else this.close();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private activate(): void {
|
|
521
|
+
const selected = this.selectedRow();
|
|
522
|
+
if (selected?.kind === "heading" && this.isNarrow()) {
|
|
523
|
+
this.narrowGroup = selected.group.id;
|
|
524
|
+
this.listScroll = 0;
|
|
525
|
+
const children = this.visibleRows();
|
|
526
|
+
if (!children.some((row) => row.id === this.selectedId)) this.select(0, children);
|
|
527
|
+
this.clearFooterLayout();
|
|
528
|
+
this.deps.requestRender();
|
|
529
|
+
} else if (this.selectedTask()) this.setNarrowView("details");
|
|
363
530
|
}
|
|
364
531
|
|
|
365
532
|
private bodyRows(): number {
|
|
366
|
-
return
|
|
533
|
+
return this.layout().bodyRows;
|
|
367
534
|
}
|
|
368
535
|
|
|
369
536
|
private refreshTasks(): void {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
this.
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
537
|
+
this.tasks = this.deps.store.list().filter((task) => task.parentSessionId === this.deps.sessionId
|
|
538
|
+
&& !isFinished(task.status) && (this.deps.isLocalTask?.(task) ?? true));
|
|
539
|
+
if (this.scope === VIEW_SCOPE.ALL) this.tasks.push(...this.peers.flatMap((group) => group.tasks));
|
|
540
|
+
const groups = this.sessionGroups();
|
|
541
|
+
const present = new Set(groups.map((group) => group.id));
|
|
542
|
+
for (const id of this.observedActiveGroups) if (!present.has(id)) this.observedActiveGroups.delete(id);
|
|
543
|
+
for (const group of groups) {
|
|
544
|
+
if (group.tasks.some((task) => !isFinished(task.status))) this.observedActiveGroups.add(group.id);
|
|
545
|
+
}
|
|
546
|
+
const rows = this.allRows();
|
|
547
|
+
if (this.narrowGroup && !this.sessionGroups().some((group) => group.id === this.narrowGroup)) this.narrowGroup = undefined;
|
|
548
|
+
if (!this.selectedId || (!this.selectedTask() && !rows.some((row) => row.id === this.selectedId))) {
|
|
549
|
+
this.selectedId = rows.find((row) => row.kind === "task")?.id ?? rows[0]?.id;
|
|
550
|
+
}
|
|
551
|
+
if (!this.selectedTask()) this.narrowView = "list";
|
|
552
|
+
this.listScroll = Math.max(0, Math.min(this.listScroll, Math.max(0, rows.length - this.bodyRows())));
|
|
553
|
+
if (this.hoveredId && !rows.some((row) => row.id === this.hoveredId)) this.hoveredId = undefined;
|
|
554
|
+
this.subscribeSelected();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private sessionGroups(): SessionGroup[] {
|
|
558
|
+
const groups = new Map<string, SessionGroup>();
|
|
559
|
+
for (const task of this.tasks) {
|
|
560
|
+
const sessionId = task.parentSessionId.trim() || undefined;
|
|
561
|
+
const id = sessionId ? `session:${sessionId}` : `unknown:${task.id}`;
|
|
562
|
+
const group = groups.get(id) ?? { id, sessionId, tasks: [] };
|
|
563
|
+
if (!groups.has(id)) groups.set(id, group);
|
|
564
|
+
group.tasks.push(task);
|
|
565
|
+
}
|
|
566
|
+
for (const group of groups.values()) {
|
|
567
|
+
group.tasks.sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
568
|
+
}
|
|
569
|
+
if (this.scope === VIEW_SCOPE.ALL) {
|
|
570
|
+
const localId = `session:${this.deps.sessionId}`;
|
|
571
|
+
const local = groups.get(localId) ?? { id: localId, sessionId: this.deps.sessionId, tasks: [] };
|
|
572
|
+
return [local, ...this.peers];
|
|
573
|
+
}
|
|
574
|
+
return [...groups.values()];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private visibleRows(): VisibleRow[] {
|
|
578
|
+
if (!this.isNarrow() || this.scope === VIEW_SCOPE.SESSION) return this.allRows();
|
|
579
|
+
const groups = this.sessionGroups();
|
|
580
|
+
if (!this.narrowGroup) return groups.map((group) => ({ id: `heading:${group.id}`, kind: "heading", group }));
|
|
581
|
+
const group = groups.find((entry) => entry.id === this.narrowGroup);
|
|
582
|
+
return group?.tasks.map((task) => ({ id: `task:${task.id}`, kind: "task", group, task })) ?? [];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private allRows(): VisibleRow[] {
|
|
586
|
+
const rows: VisibleRow[] = [];
|
|
587
|
+
for (const group of this.sessionGroups()) {
|
|
588
|
+
if (this.scope === VIEW_SCOPE.ALL) rows.push({ id: `heading:${group.id}`, kind: "heading", group });
|
|
589
|
+
if (this.scope === VIEW_SCOPE.SESSION || this.isExpanded(group)) {
|
|
590
|
+
for (const task of group.tasks) rows.push({ id: `task:${task.id}`, kind: "task", group, task });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return rows;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private selectedRow(rows = this.visibleRows()): VisibleRow | undefined {
|
|
597
|
+
if (this.directoryRoot() && this.narrowView === "list") return rows.find((row) => row.id === this.groupCursor) ?? rows[0];
|
|
598
|
+
return rows.find((row) => row.id === this.selectedId);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private isExpanded(group: SessionGroup): boolean {
|
|
602
|
+
return this.expanded.get(group.id) ?? this.observedActiveGroups.has(group.id);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private setExpanded(group: SessionGroup, expanded: boolean): void {
|
|
606
|
+
if (this.isExpanded(group) === expanded) return;
|
|
607
|
+
this.expanded.set(group.id, expanded);
|
|
608
|
+
if (!expanded) {
|
|
609
|
+
this.selectedId = `heading:${group.id}`;
|
|
610
|
+
this.narrowView = "list";
|
|
611
|
+
}
|
|
612
|
+
this.listScroll = Math.min(this.listScroll, Math.max(0, this.visibleRows().length - this.bodyRows()));
|
|
613
|
+
this.clearFooterLayout();
|
|
614
|
+
this.subscribeSelected();
|
|
615
|
+
this.deps.requestRender();
|
|
378
616
|
}
|
|
379
617
|
|
|
380
618
|
private subscribeSelected(): void {
|
|
381
|
-
this.unsubscribeTask?.();
|
|
382
619
|
const task = this.selectedTask();
|
|
383
|
-
|
|
620
|
+
if (task?.id === this.subscribedTaskId) return;
|
|
621
|
+
this.unsubscribeTask?.();
|
|
622
|
+
this.unsubscribeTask = undefined;
|
|
623
|
+
this.subscribedTaskId = task?.id;
|
|
624
|
+
if (!task || this.remoteThreads.has(task.id)) return;
|
|
625
|
+
this.unsubscribeTask = this.deps.store.subscribe(task.id, () => {
|
|
384
626
|
this.clearFooterLayout();
|
|
385
627
|
this.refreshTasks();
|
|
386
628
|
this.deps.requestRender();
|
|
387
|
-
})
|
|
629
|
+
});
|
|
388
630
|
}
|
|
389
631
|
|
|
390
632
|
private taskLine(row: number): string {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
if (!
|
|
633
|
+
const visible = this.visibleRows();
|
|
634
|
+
if (visible.length === 0) return row === 0 ? this.deps.theme.fg(ROLE.EMPTY, EMPTY_LIST) : "";
|
|
635
|
+
const entry = visible[this.listScroll + row];
|
|
636
|
+
if (!entry) return "";
|
|
395
637
|
const theme = this.deps.theme;
|
|
396
638
|
this.taskRegion(row).render(this.pointerLayout?.listWidth ?? 1);
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
const
|
|
639
|
+
const selected = entry.id === this.selectedRow()?.id;
|
|
640
|
+
const hovered = entry.id === this.hoveredId;
|
|
641
|
+
const emphasis = selected ? ROLE.SELECTED : hovered ? ROLE.HOVER : undefined;
|
|
642
|
+
const marker = emphasis ? theme.fg(emphasis, selected ? "▸" : "▹") : " ";
|
|
643
|
+
if (entry.kind === "heading") {
|
|
644
|
+
const state = this.isExpanded(entry.group) ? "▾" : "▸";
|
|
645
|
+
return `${marker}${theme.fg(emphasis ?? ROLE.SELECTED, state)} ${theme.fg(emphasis ?? ROLE.NAME_IDLE, this.groupHeading(entry.group))}`;
|
|
646
|
+
}
|
|
647
|
+
const task = entry.task;
|
|
648
|
+
const glyph = theme.fg(GLYPH_ROLE[task.status] ?? ROLE.META, GLYPH[task.status] ?? "?");
|
|
649
|
+
const name = theme.fg(emphasis ?? ROLE.NAME_IDLE, `Subagent ${task.agent}`);
|
|
400
650
|
const time = task.startedAt === null ? "" : theme.fg(ROLE.META, formatElapsed((task.endedAt ?? this.deps.now()) - task.startedAt));
|
|
401
|
-
return `${marker} ${glyph} ${name} ${time}`;
|
|
651
|
+
return `${marker} ${theme.fg(ROLE.META, "└")} ${glyph} ${name} ${time}`;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private groupHeading(group: SessionGroup): string {
|
|
655
|
+
const count = `${group.tasks.length} ${group.tasks.length === 1 ? "Subagent" : "Subagents"}`;
|
|
656
|
+
if (!this.isExpanded(group)) return `${this.groupTitle(group)} · ${count}`;
|
|
657
|
+
const active = group.tasks.filter((task) => !isFinished(task.status)).length;
|
|
658
|
+
return `${this.groupTitle(group)} · ${count} · ${active} active`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private groupTitle(group: SessionGroup): string {
|
|
662
|
+
if (group.label) return group.label;
|
|
663
|
+
if (!group.sessionId) return "Current orchestrator";
|
|
664
|
+
if (group.sessionId === this.deps.sessionId) return "Current orchestrator";
|
|
665
|
+
return `Orchestrator ${group.sessionId.slice(0, 8)}`;
|
|
402
666
|
}
|
|
403
667
|
|
|
404
668
|
private threadLines(thread: TaskThread, width: number): string[] {
|
|
@@ -411,7 +675,7 @@ export class AgentsView {
|
|
|
411
675
|
for (const item of thread.items) {
|
|
412
676
|
let rendered = this.cache.get(item);
|
|
413
677
|
if (!rendered) {
|
|
414
|
-
rendered =
|
|
678
|
+
rendered = renderThreadItem(item, this.deps.theme, width);
|
|
415
679
|
this.cache.set(item, rendered);
|
|
416
680
|
}
|
|
417
681
|
lines.push(...rendered);
|
|
@@ -421,23 +685,37 @@ export class AgentsView {
|
|
|
421
685
|
|
|
422
686
|
private threadWindow(rows: number, width: number): string[] {
|
|
423
687
|
const task = this.selectedTask();
|
|
424
|
-
if (!task) return [];
|
|
688
|
+
if (!task) return [this.deps.theme.fg(ROLE.EMPTY, "Select a task to inspect its thread")];
|
|
425
689
|
const theme = this.deps.theme;
|
|
426
690
|
const header = theme.fg(ROLE.META, truncateToWidth(taskHeader(task, this.deps.now()), width, "…"));
|
|
427
|
-
const lines = this.threadLines(this.deps.store.thread(task.id), width);
|
|
691
|
+
const lines = this.threadLines(this.remoteThreads.get(task.id) ?? this.deps.store.thread(task.id), width);
|
|
428
692
|
if (lines.length === 0) return [header, theme.fg(ROLE.EMPTY, task.error ?? EMPTY_THREAD)];
|
|
429
|
-
const visible = rows - 1;
|
|
693
|
+
const visible = Math.max(0, rows - 1);
|
|
430
694
|
const maxScroll = Math.max(0, lines.length - visible);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
695
|
+
if (this.follow) this.scroll = maxScroll;
|
|
696
|
+
// A taller/wider presentation may clamp its window, not the saved position.
|
|
697
|
+
const start = Math.min(this.scroll, maxScroll);
|
|
698
|
+
return [header, ...lines.slice(start, start + visible)];
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private select(index: number, rows = this.visibleRows(), revealSelection = true): void {
|
|
702
|
+
const next = rows[Math.max(0, Math.min(rows.length - 1, index))];
|
|
703
|
+
if (!next) return;
|
|
704
|
+
// Keyboard movement reveals the selection; pointer activation keeps the
|
|
705
|
+
// manually positioned list available for Back, even after height clamping.
|
|
706
|
+
if (revealSelection) this.manualListOffsets.delete(this.listKey());
|
|
707
|
+
if (this.directoryRoot()) {
|
|
708
|
+
this.groupCursor = next.id;
|
|
709
|
+
this.clearFooterLayout();
|
|
710
|
+
this.deps.requestRender();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (next.id === this.selectedId) return;
|
|
714
|
+
this.selectedId = next.id;
|
|
715
|
+
if (next.kind === "heading") this.narrowView = "list";
|
|
439
716
|
this.scroll = 0;
|
|
440
717
|
this.follow = true;
|
|
718
|
+
this.clearFooterLayout();
|
|
441
719
|
this.subscribeSelected();
|
|
442
720
|
this.deps.requestRender();
|
|
443
721
|
}
|
|
@@ -460,34 +738,91 @@ export class AgentsView {
|
|
|
460
738
|
}
|
|
461
739
|
|
|
462
740
|
private footer(keys: string, width: number): string {
|
|
463
|
-
const
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
this.
|
|
469
|
-
this.
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
741
|
+
const task = this.actionableTask();
|
|
742
|
+
const controls = [
|
|
743
|
+
{ label: FOLLOW_BUTTON, short: "Follow", region: this.followRegion, enabled: Boolean(task) },
|
|
744
|
+
{ label: OPEN_BUTTON, short: "Open", region: this.openRegion, enabled: this.canOpen(task) },
|
|
745
|
+
];
|
|
746
|
+
if (task && this.canCancel(task)) controls.push({ label: STOP_BUTTON, short: "Stop", region: this.actionRegion("stop", () => this.handleInput("s")), enabled: true });
|
|
747
|
+
if (this.deps.sessionId !== undefined) controls.push({ label: SCOPE_BUTTON, short: "Scope", region: this.actionRegion("scope", () => this.toggleScope()), enabled: true });
|
|
748
|
+
for (const control of controls) control.region.setDisabled(!control.enabled);
|
|
749
|
+
const required = controls.reduce((sum, control) => sum + control.label.length + 1, -1);
|
|
750
|
+
const shown = required <= width ? controls : [controls[this.footerPage % controls.length]];
|
|
751
|
+
const actions: NonNullable<PointerLayout["actions"]> = [];
|
|
752
|
+
let text = "";
|
|
753
|
+
for (const control of shown) {
|
|
754
|
+
const label = required <= width ? control.label : control.short;
|
|
755
|
+
if (text) text += " ";
|
|
756
|
+
actions.push({ x: 2 + visibleWidth(text), width: label.length, region: control.region });
|
|
757
|
+
control.region.render(label.length);
|
|
758
|
+
const hovered = (control.region === this.followRegion && this.hoveredControl === "follow") || (control.region === this.openRegion && this.hoveredControl === "open");
|
|
759
|
+
text += this.deps.theme.fg(!control.enabled ? ROLE.META : hovered ? ROLE.HOVER : ROLE.KEY, label);
|
|
760
|
+
}
|
|
761
|
+
if (required > width) {
|
|
762
|
+
const more = this.actionRegion("more", () => {
|
|
763
|
+
this.footerPage++;
|
|
764
|
+
this.clearFooterLayout();
|
|
765
|
+
this.deps.requestRender();
|
|
766
|
+
});
|
|
767
|
+
more.render(1);
|
|
768
|
+
actions.push({ x: 2 + width - 1, width: 1, region: more });
|
|
769
|
+
text = fit(text, width - 1) + ">";
|
|
770
|
+
} else if (visibleWidth(text) + 1 < width) text += " " + truncateToWidth(keys, width - visibleWidth(text) - 1, "…");
|
|
771
|
+
if (this.pointerLayout) this.pointerLayout.actions = actions;
|
|
772
|
+
return text;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
private actionRegion(id: string, action: () => void): NativePointerRegion {
|
|
776
|
+
let region = this.actionRegions.get(id);
|
|
777
|
+
if (!region) {
|
|
778
|
+
region = this.pointerScope.wrap(EMPTY_COMPONENT, {
|
|
779
|
+
onClick: (event) => {
|
|
780
|
+
if (event.button !== "left") return undefined;
|
|
781
|
+
action();
|
|
782
|
+
return { handled: true, render: true };
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
this.actionRegions.set(id, region);
|
|
474
786
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
787
|
+
return region;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private modeLabel(layout: AgentsViewLayout): string | undefined {
|
|
791
|
+
if (this.narrowView === "details" || (this.isNarrow() && this.narrowGroup)) return layout.width < 24 ? "←" : "[← Back]";
|
|
792
|
+
return !this.isNarrow() && this.selectedTask() ? "[F Fullscreen]" : undefined;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
private closeLabel(layout: AgentsViewLayout): string | undefined {
|
|
796
|
+
if (layout.mode === "fallback") return undefined;
|
|
797
|
+
return layout.width < 60 ? "[×]" : CLOSE_BUTTON;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private setNarrowView(view: "list" | "details"): void {
|
|
801
|
+
if (view === this.narrowView || (view === "details" && !this.selectedTask())) return;
|
|
802
|
+
this.narrowView = view;
|
|
803
|
+
this.clearFooterLayout();
|
|
804
|
+
this.deps.requestRender();
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
private close(): void {
|
|
808
|
+
if (this.closed) return;
|
|
809
|
+
this.closed = true;
|
|
810
|
+
this.stopPresenceRefresh();
|
|
811
|
+
this.pointerScope.invalidate();
|
|
812
|
+
this.deps.onClose();
|
|
478
813
|
}
|
|
479
814
|
|
|
480
815
|
private isInButton(x: number, button: PointerButtonLayout | undefined): boolean {
|
|
481
816
|
return button !== undefined && x >= button.x && x < button.x + button.width;
|
|
482
817
|
}
|
|
483
818
|
|
|
484
|
-
private hoverControl(control: "follow" | "open"): TuiMouseEventResult {
|
|
819
|
+
private hoverControl(control: "follow" | "open" | "close" | "mode"): TuiMouseEventResult {
|
|
485
820
|
if (this.hoveredControl === control) return { handled: true };
|
|
486
821
|
this.hoveredControl = control;
|
|
487
822
|
return { handled: true, render: true };
|
|
488
823
|
}
|
|
489
824
|
|
|
490
|
-
private clearHoveredControl(control: "follow" | "open"): void {
|
|
825
|
+
private clearHoveredControl(control: "follow" | "open" | "close" | "mode"): void {
|
|
491
826
|
if (this.hoveredControl !== control) return;
|
|
492
827
|
this.hoveredControl = undefined;
|
|
493
828
|
this.deps.requestRender();
|
|
@@ -507,6 +842,19 @@ export class AgentsView {
|
|
|
507
842
|
return { handled: true, render: true };
|
|
508
843
|
}
|
|
509
844
|
|
|
845
|
+
private clickClose(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
846
|
+
if (event.button !== "left") return undefined;
|
|
847
|
+
this.close();
|
|
848
|
+
return { handled: true, render: true };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private clickMode(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
852
|
+
if (event.button !== "left") return undefined;
|
|
853
|
+
if (this.narrowView === "details" || (this.isNarrow() && this.narrowGroup)) this.back();
|
|
854
|
+
else this.activate();
|
|
855
|
+
return { handled: true, render: true };
|
|
856
|
+
}
|
|
857
|
+
|
|
510
858
|
private taskRegion(row: number): NativePointerRegion {
|
|
511
859
|
let region = this.taskRegions.get(row);
|
|
512
860
|
if (!region) {
|
|
@@ -521,30 +869,39 @@ export class AgentsView {
|
|
|
521
869
|
}
|
|
522
870
|
|
|
523
871
|
private hoverTask(row: number): TuiMouseEventResult | undefined {
|
|
524
|
-
const
|
|
525
|
-
if (!
|
|
526
|
-
this.
|
|
872
|
+
const entry = this.visibleRows()[this.listScroll + row];
|
|
873
|
+
if (!entry || this.hoveredId === entry.id) return { handled: true };
|
|
874
|
+
this.hoveredId = entry.id;
|
|
527
875
|
return { handled: true, render: true };
|
|
528
876
|
}
|
|
529
877
|
|
|
530
878
|
private clearHoveredTask(row: number): void {
|
|
531
|
-
|
|
532
|
-
this.
|
|
879
|
+
const entry = this.visibleRows()[this.listScroll + row];
|
|
880
|
+
if (!entry || this.hoveredId !== entry.id) return;
|
|
881
|
+
this.hoveredId = undefined;
|
|
533
882
|
this.deps.requestRender();
|
|
534
883
|
}
|
|
535
884
|
|
|
536
885
|
private clickTask(row: number, event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
537
886
|
if (event.button !== "left") return undefined;
|
|
538
|
-
this.
|
|
887
|
+
const entry = this.visibleRows()[this.listScroll + row];
|
|
888
|
+
if (!entry) return undefined;
|
|
889
|
+
if (this.isNarrow()) {
|
|
890
|
+
this.select(this.listScroll + row, this.visibleRows(), false);
|
|
891
|
+
this.activate();
|
|
892
|
+
} else if (entry.kind === "heading") this.setExpanded(entry.group, !this.isExpanded(entry.group));
|
|
893
|
+
else this.select(this.listScroll + row);
|
|
539
894
|
return { handled: true, render: true };
|
|
540
895
|
}
|
|
541
896
|
|
|
542
897
|
private wheelList(event: TuiMouseEvent): TuiMouseEventResult {
|
|
543
898
|
const delta = event.wheelDelta ?? 0;
|
|
544
|
-
const next = Math.max(0, Math.min(Math.max(0, this.
|
|
899
|
+
const next = Math.max(0, Math.min(Math.max(0, this.visibleRows().length - this.bodyRows()), this.listScroll + delta));
|
|
545
900
|
if (next === this.listScroll) return { handled: true, render: false };
|
|
546
901
|
this.listScroll = next;
|
|
547
|
-
this.
|
|
902
|
+
this.manualListOffsets.set(this.listKey(), next);
|
|
903
|
+
this.clearFooterLayout();
|
|
904
|
+
this.hoveredId = undefined;
|
|
548
905
|
return { handled: true, render: true };
|
|
549
906
|
}
|
|
550
907
|
|