pi-messenger 0.7.3
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/ARCHITECTURE.md +244 -0
- package/CHANGELOG.md +418 -0
- package/README.md +394 -0
- package/banner.png +0 -0
- package/config-overlay.ts +172 -0
- package/config.ts +178 -0
- package/crew/agents/crew-docs-scout.md +55 -0
- package/crew/agents/crew-gap-analyst.md +105 -0
- package/crew/agents/crew-github-scout.md +111 -0
- package/crew/agents/crew-interview-generator.md +79 -0
- package/crew/agents/crew-plan-sync.md +64 -0
- package/crew/agents/crew-practice-scout.md +62 -0
- package/crew/agents/crew-repo-scout.md +65 -0
- package/crew/agents/crew-reviewer.md +58 -0
- package/crew/agents/crew-web-scout.md +85 -0
- package/crew/agents/crew-worker.md +95 -0
- package/crew/agents.ts +200 -0
- package/crew/handlers/interview.ts +211 -0
- package/crew/handlers/plan.ts +358 -0
- package/crew/handlers/review.ts +341 -0
- package/crew/handlers/status.ts +257 -0
- package/crew/handlers/sync.ts +232 -0
- package/crew/handlers/task.ts +511 -0
- package/crew/handlers/work.ts +289 -0
- package/crew/id-allocator.ts +44 -0
- package/crew/index.ts +229 -0
- package/crew/state.ts +116 -0
- package/crew/store.ts +480 -0
- package/crew/types.ts +164 -0
- package/crew/utils/artifacts.ts +65 -0
- package/crew/utils/config.ts +104 -0
- package/crew/utils/discover.ts +170 -0
- package/crew/utils/install.ts +373 -0
- package/crew/utils/progress.ts +107 -0
- package/crew/utils/result.ts +16 -0
- package/crew/utils/truncate.ts +79 -0
- package/crew-overlay.ts +259 -0
- package/handlers.ts +799 -0
- package/index.ts +591 -0
- package/lib.ts +232 -0
- package/overlay.ts +687 -0
- package/package.json +20 -0
- package/skills/pi-messenger-crew/SKILL.md +140 -0
- package/store.ts +1068 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew - Progress Tracking
|
|
3
|
+
*
|
|
4
|
+
* Real-time visibility into agent execution via --mode json event parsing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface AgentProgress {
|
|
8
|
+
agent: string;
|
|
9
|
+
status: "pending" | "running" | "completed" | "failed";
|
|
10
|
+
currentTool?: string;
|
|
11
|
+
currentToolArgs?: string;
|
|
12
|
+
recentTools: Array<{ tool: string; args: string; endMs: number }>;
|
|
13
|
+
tokens: number;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Event types from pi's --mode json output
|
|
19
|
+
interface PiEvent {
|
|
20
|
+
type: string;
|
|
21
|
+
toolName?: string;
|
|
22
|
+
args?: Record<string, unknown>;
|
|
23
|
+
message?: {
|
|
24
|
+
role: string;
|
|
25
|
+
usage?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
|
|
26
|
+
model?: string;
|
|
27
|
+
content?: Array<{ type: string; text?: string }>;
|
|
28
|
+
errorMessage?: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createProgress(agent: string): AgentProgress {
|
|
33
|
+
return {
|
|
34
|
+
agent,
|
|
35
|
+
status: "pending",
|
|
36
|
+
recentTools: [],
|
|
37
|
+
tokens: 0,
|
|
38
|
+
durationMs: 0,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseJsonlLine(line: string): PiEvent | null {
|
|
43
|
+
if (!line.trim()) return null;
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(line);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function updateProgress(progress: AgentProgress, event: PiEvent, startTime: number): void {
|
|
52
|
+
progress.durationMs = Date.now() - startTime;
|
|
53
|
+
|
|
54
|
+
switch (event.type) {
|
|
55
|
+
case "tool_execution_start":
|
|
56
|
+
progress.status = "running";
|
|
57
|
+
progress.currentTool = event.toolName;
|
|
58
|
+
progress.currentToolArgs = extractArgsPreview(event.args);
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case "tool_execution_end":
|
|
62
|
+
if (progress.currentTool) {
|
|
63
|
+
progress.recentTools.unshift({
|
|
64
|
+
tool: progress.currentTool,
|
|
65
|
+
args: progress.currentToolArgs ?? "",
|
|
66
|
+
endMs: Date.now(),
|
|
67
|
+
});
|
|
68
|
+
if (progress.recentTools.length > 5) progress.recentTools.pop();
|
|
69
|
+
}
|
|
70
|
+
progress.currentTool = undefined;
|
|
71
|
+
progress.currentToolArgs = undefined;
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case "message_end":
|
|
75
|
+
if (event.message?.usage) {
|
|
76
|
+
progress.tokens += (event.message.usage.input ?? 0) + (event.message.usage.output ?? 0);
|
|
77
|
+
}
|
|
78
|
+
if (event.message?.errorMessage) {
|
|
79
|
+
progress.error = event.message.errorMessage;
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function extractArgsPreview(args?: Record<string, unknown>): string {
|
|
86
|
+
if (!args) return "";
|
|
87
|
+
const previewKeys = ["command", "path", "file_path", "pattern", "query"];
|
|
88
|
+
for (const key of previewKeys) {
|
|
89
|
+
if (args[key] && typeof args[key] === "string") {
|
|
90
|
+
const value = args[key] as string;
|
|
91
|
+
return value.length > 60 ? `${value.slice(0, 57)}...` : value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getFinalOutput(messages: PiEvent[]): string {
|
|
98
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
99
|
+
const msg = messages[i];
|
|
100
|
+
if (msg.type === "message_end" && msg.message?.role === "assistant") {
|
|
101
|
+
for (const part of msg.message.content ?? []) {
|
|
102
|
+
if (part.type === "text" && part.text) return part.text;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return "";
|
|
107
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew - Result Formatter
|
|
3
|
+
*
|
|
4
|
+
* Helper for consistent tool result formatting.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format a tool result with text content and structured details.
|
|
9
|
+
* Matches the pattern used throughout pi-messenger handlers.
|
|
10
|
+
*/
|
|
11
|
+
export function result(text: string, details: Record<string, unknown>) {
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: "text" as const, text }],
|
|
14
|
+
details
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew - Output Truncation
|
|
3
|
+
*
|
|
4
|
+
* Prevents token explosion from verbose agent outputs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface MaxOutputConfig {
|
|
8
|
+
bytes?: number;
|
|
9
|
+
lines?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TruncationResult {
|
|
13
|
+
text: string;
|
|
14
|
+
truncated: boolean;
|
|
15
|
+
originalBytes?: number;
|
|
16
|
+
originalLines?: number;
|
|
17
|
+
artifactPath?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_MAX_OUTPUT: Required<MaxOutputConfig> = {
|
|
21
|
+
bytes: 200 * 1024, // 200KB
|
|
22
|
+
lines: 5000,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function formatBytes(bytes: number): string {
|
|
26
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
27
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
28
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Truncate output to fit within limits.
|
|
33
|
+
*/
|
|
34
|
+
export function truncateOutput(
|
|
35
|
+
output: string,
|
|
36
|
+
config: MaxOutputConfig,
|
|
37
|
+
artifactPath?: string
|
|
38
|
+
): TruncationResult {
|
|
39
|
+
const maxBytes = config.bytes ?? DEFAULT_MAX_OUTPUT.bytes;
|
|
40
|
+
const maxLines = config.lines ?? DEFAULT_MAX_OUTPUT.lines;
|
|
41
|
+
|
|
42
|
+
const lines = output.split("\n");
|
|
43
|
+
const bytes = Buffer.byteLength(output, "utf-8");
|
|
44
|
+
|
|
45
|
+
if (bytes <= maxBytes && lines.length <= maxLines) {
|
|
46
|
+
return { text: output, truncated: false };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Truncate by lines first
|
|
50
|
+
let truncatedLines = lines.length > maxLines ? lines.slice(0, maxLines) : lines;
|
|
51
|
+
let result = truncatedLines.join("\n");
|
|
52
|
+
|
|
53
|
+
// Then truncate by bytes if still too large
|
|
54
|
+
if (Buffer.byteLength(result, "utf-8") > maxBytes) {
|
|
55
|
+
// Binary search for the right cut point
|
|
56
|
+
let low = 0, high = result.length;
|
|
57
|
+
while (low < high) {
|
|
58
|
+
const mid = Math.floor((low + high + 1) / 2);
|
|
59
|
+
if (Buffer.byteLength(result.slice(0, mid), "utf-8") <= maxBytes) {
|
|
60
|
+
low = mid;
|
|
61
|
+
} else {
|
|
62
|
+
high = mid - 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
result = result.slice(0, low);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const keptLines = result.split("\n").length;
|
|
69
|
+
const fullOutputHint = artifactPath ? ` - full output at ${artifactPath}` : "";
|
|
70
|
+
const marker = `[TRUNCATED: ${keptLines}/${lines.length} lines, ${formatBytes(Buffer.byteLength(result))}/${formatBytes(bytes)}${fullOutputHint}]\n`;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
text: marker + result,
|
|
74
|
+
truncated: true,
|
|
75
|
+
originalBytes: bytes,
|
|
76
|
+
originalLines: lines.length,
|
|
77
|
+
artifactPath,
|
|
78
|
+
};
|
|
79
|
+
}
|
package/crew-overlay.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew Overlay - Task Visualization
|
|
3
|
+
*
|
|
4
|
+
* Renders the Crew tab content for the messenger overlay.
|
|
5
|
+
* Shows flat task list under PRD name with status and dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
9
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import * as crewStore from "./crew/store.js";
|
|
11
|
+
import { autonomousState } from "./crew/state.js";
|
|
12
|
+
import type { Task } from "./crew/types.js";
|
|
13
|
+
|
|
14
|
+
// Status icons
|
|
15
|
+
const STATUS_ICONS: Record<string, string> = {
|
|
16
|
+
done: "✓",
|
|
17
|
+
in_progress: "●",
|
|
18
|
+
todo: "○",
|
|
19
|
+
blocked: "✗",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface CrewViewState {
|
|
23
|
+
scrollOffset: number;
|
|
24
|
+
selectedTaskIndex: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createCrewViewState(): CrewViewState {
|
|
28
|
+
return {
|
|
29
|
+
scrollOffset: 0,
|
|
30
|
+
selectedTaskIndex: 0,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Render the crew overview content - flat task list under PRD.
|
|
36
|
+
*/
|
|
37
|
+
export function renderCrewContent(
|
|
38
|
+
theme: Theme,
|
|
39
|
+
cwd: string,
|
|
40
|
+
width: number,
|
|
41
|
+
height: number,
|
|
42
|
+
viewState: CrewViewState
|
|
43
|
+
): string[] {
|
|
44
|
+
const lines: string[] = [];
|
|
45
|
+
const plan = crewStore.getPlan(cwd);
|
|
46
|
+
|
|
47
|
+
if (!plan) {
|
|
48
|
+
return renderEmptyState(theme, width, height);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tasks = crewStore.getTasks(cwd);
|
|
52
|
+
|
|
53
|
+
// Header: PRD with progress
|
|
54
|
+
const pct = plan.task_count > 0 ? Math.round((plan.completed_count / plan.task_count) * 100) : 0;
|
|
55
|
+
const progressText = `[${plan.completed_count}/${plan.task_count}]`;
|
|
56
|
+
const prdLine = `📋 ${plan.prd}`;
|
|
57
|
+
const prdWidth = visibleWidth(prdLine);
|
|
58
|
+
const progressWidth = visibleWidth(progressText);
|
|
59
|
+
const padding = Math.max(1, width - prdWidth - progressWidth - 2);
|
|
60
|
+
|
|
61
|
+
lines.push(prdLine + " ".repeat(padding) + theme.fg("accent", progressText));
|
|
62
|
+
lines.push("");
|
|
63
|
+
|
|
64
|
+
// Task list
|
|
65
|
+
if (tasks.length === 0) {
|
|
66
|
+
lines.push(theme.fg("dim", " (no tasks yet)"));
|
|
67
|
+
} else {
|
|
68
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
69
|
+
const task = tasks[i];
|
|
70
|
+
const taskLine = renderTaskLine(theme, task, i === viewState.selectedTaskIndex, width);
|
|
71
|
+
lines.push(taskLine);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Add legend
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push(renderLegend(theme, width));
|
|
78
|
+
|
|
79
|
+
// Ensure we fill the height
|
|
80
|
+
while (lines.length < height) {
|
|
81
|
+
lines.push("");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle scrolling if content exceeds height
|
|
85
|
+
if (lines.length > height) {
|
|
86
|
+
const startIdx = Math.min(viewState.scrollOffset, lines.length - height);
|
|
87
|
+
return lines.slice(startIdx, startIdx + height);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return lines.slice(0, height);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Render the status bar for autonomous mode.
|
|
95
|
+
*/
|
|
96
|
+
export function renderCrewStatusBar(theme: Theme, cwd: string, width: number): string {
|
|
97
|
+
const plan = crewStore.getPlan(cwd);
|
|
98
|
+
|
|
99
|
+
if (!plan) {
|
|
100
|
+
return theme.fg("dim", "No active plan");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!autonomousState.active) {
|
|
104
|
+
// Show plan progress
|
|
105
|
+
const progress = `${plan.completed_count}/${plan.task_count}`;
|
|
106
|
+
const ready = crewStore.getReadyTasks(cwd);
|
|
107
|
+
const readyText = ready.length > 0 ? ` │ ${ready.length} ready` : "";
|
|
108
|
+
return truncateToWidth(
|
|
109
|
+
`📋 ${plan.prd}: ${progress} tasks${readyText}`,
|
|
110
|
+
width
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Autonomous mode active
|
|
115
|
+
const progress = `${plan.completed_count}/${plan.task_count}`;
|
|
116
|
+
|
|
117
|
+
// Calculate elapsed time
|
|
118
|
+
let elapsed = "";
|
|
119
|
+
if (autonomousState.startedAt) {
|
|
120
|
+
const startTime = new Date(autonomousState.startedAt).getTime();
|
|
121
|
+
const elapsedMs = Date.now() - startTime;
|
|
122
|
+
const minutes = Math.floor(elapsedMs / 60000);
|
|
123
|
+
const seconds = Math.floor((elapsedMs % 60000) / 1000);
|
|
124
|
+
elapsed = `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const readyTasks = crewStore.getReadyTasks(cwd);
|
|
128
|
+
|
|
129
|
+
const parts = [
|
|
130
|
+
`Wave ${autonomousState.waveNumber}`,
|
|
131
|
+
`${progress} tasks`,
|
|
132
|
+
`${readyTasks.length} ready`,
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
if (elapsed) {
|
|
136
|
+
parts.push(`⏱️ ${elapsed}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return truncateToWidth(
|
|
140
|
+
theme.fg("accent", "● AUTO ") + parts.join(" │ "),
|
|
141
|
+
width
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// Private Helpers
|
|
147
|
+
// =============================================================================
|
|
148
|
+
|
|
149
|
+
function renderEmptyState(theme: Theme, width: number, height: number): string[] {
|
|
150
|
+
const lines: string[] = [];
|
|
151
|
+
const msg = "No active plan";
|
|
152
|
+
const hint = "Use pi_messenger({ action: \"plan\" })";
|
|
153
|
+
|
|
154
|
+
const padTop = Math.floor((height - 3) / 2);
|
|
155
|
+
for (let i = 0; i < padTop; i++) lines.push("");
|
|
156
|
+
|
|
157
|
+
const pad1 = " ".repeat(Math.max(0, Math.floor((width - msg.length) / 2)));
|
|
158
|
+
lines.push(pad1 + msg);
|
|
159
|
+
lines.push("");
|
|
160
|
+
const pad2 = " ".repeat(Math.max(0, Math.floor((width - hint.length) / 2)));
|
|
161
|
+
lines.push(pad2 + theme.fg("dim", hint));
|
|
162
|
+
|
|
163
|
+
while (lines.length < height) lines.push("");
|
|
164
|
+
return lines;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderTaskLine(
|
|
168
|
+
theme: Theme,
|
|
169
|
+
task: Task,
|
|
170
|
+
isSelected: boolean,
|
|
171
|
+
width: number
|
|
172
|
+
): string {
|
|
173
|
+
const icon = STATUS_ICONS[task.status] ?? "?";
|
|
174
|
+
const selectIndicator = isSelected ? theme.fg("accent", "▸ ") : " ";
|
|
175
|
+
|
|
176
|
+
// Color the icon based on status
|
|
177
|
+
let coloredIcon: string;
|
|
178
|
+
switch (task.status) {
|
|
179
|
+
case "done":
|
|
180
|
+
coloredIcon = theme.fg("accent", icon);
|
|
181
|
+
break;
|
|
182
|
+
case "in_progress":
|
|
183
|
+
coloredIcon = theme.fg("warning", icon);
|
|
184
|
+
break;
|
|
185
|
+
case "blocked":
|
|
186
|
+
coloredIcon = theme.fg("error", icon);
|
|
187
|
+
break;
|
|
188
|
+
default:
|
|
189
|
+
coloredIcon = theme.fg("dim", icon);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Build task suffix (assigned agent or dependencies)
|
|
193
|
+
let suffix = "";
|
|
194
|
+
if (task.status === "in_progress" && task.assigned_to) {
|
|
195
|
+
suffix = ` (${task.assigned_to})`;
|
|
196
|
+
} else if (task.status === "todo" && task.depends_on.length > 0) {
|
|
197
|
+
suffix = ` → deps: ${task.depends_on.join(", ")}`;
|
|
198
|
+
} else if (task.status === "blocked" && task.blocked_reason) {
|
|
199
|
+
// Truncate block reason
|
|
200
|
+
const reason = task.blocked_reason.slice(0, 20);
|
|
201
|
+
suffix = ` [${reason}${task.blocked_reason.length > 20 ? "…" : ""}]`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const line = `${selectIndicator}${coloredIcon} ${task.id} ${task.title}`;
|
|
205
|
+
const fullLine = line + theme.fg("dim", suffix);
|
|
206
|
+
|
|
207
|
+
return truncateToWidth(fullLine, width);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function renderLegend(theme: Theme, width: number): string {
|
|
211
|
+
const items = [
|
|
212
|
+
`${theme.fg("accent", STATUS_ICONS.done)} done`,
|
|
213
|
+
`${theme.fg("warning", STATUS_ICONS.in_progress)} in_progress`,
|
|
214
|
+
`${theme.fg("dim", STATUS_ICONS.todo)} todo`,
|
|
215
|
+
`${theme.fg("error", STATUS_ICONS.blocked)} blocked`,
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const legend = "Legend: " + items.join(" ");
|
|
219
|
+
return truncateToWidth(theme.fg("dim", legend), width);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Navigate to next/prev task.
|
|
224
|
+
*/
|
|
225
|
+
export function navigateTask(viewState: CrewViewState, direction: 1 | -1, taskCount: number): void {
|
|
226
|
+
if (taskCount === 0) return;
|
|
227
|
+
viewState.selectedTaskIndex = Math.max(
|
|
228
|
+
0,
|
|
229
|
+
Math.min(taskCount - 1, viewState.selectedTaskIndex + direction)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get the currently selected task ID.
|
|
235
|
+
*/
|
|
236
|
+
export function getSelectedTaskId(cwd: string, viewState: CrewViewState): string | null {
|
|
237
|
+
const tasks = crewStore.getTasks(cwd);
|
|
238
|
+
if (viewState.selectedTaskIndex >= 0 && viewState.selectedTaskIndex < tasks.length) {
|
|
239
|
+
return tasks[viewState.selectedTaskIndex].id;
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Legacy exports for compatibility (no-ops now that epics are removed)
|
|
245
|
+
export function toggleEpicExpansion(_viewState: CrewViewState, _epicId: string): void {
|
|
246
|
+
// No-op - epics removed
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function navigateEpic(viewState: CrewViewState, direction: 1 | -1, _epicCount: number): void {
|
|
250
|
+
// Redirect to task navigation
|
|
251
|
+
const cwd = process.cwd();
|
|
252
|
+
const tasks = crewStore.getTasks(cwd);
|
|
253
|
+
navigateTask(viewState, direction, tasks.length);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function getSelectedEpicId(_cwd: string, _viewState: CrewViewState): string | null {
|
|
257
|
+
// No-op - epics removed
|
|
258
|
+
return null;
|
|
259
|
+
}
|