@phi-code-admin/phi-code 0.83.0 → 0.84.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/CHANGELOG.md +25 -0
- package/extensions/phi/btw/LICENSE +21 -0
- package/extensions/phi/btw/btw-ui.ts +238 -0
- package/extensions/phi/btw/btw.ts +363 -0
- package/extensions/phi/btw/index.ts +17 -0
- package/extensions/phi/btw/prompts/btw-system.txt +9 -0
- package/extensions/phi/chrome/LICENSE +21 -0
- package/extensions/phi/chrome/browser-extension/manifest.json +26 -0
- package/extensions/phi/chrome/browser-extension/service_worker.js +2388 -0
- package/extensions/phi/chrome/browser-extension/snapshot_injected.js +677 -0
- package/extensions/phi/chrome/index.ts +1799 -0
- package/extensions/phi/goal/LICENSE +21 -0
- package/extensions/phi/goal/index.ts +784 -0
- package/extensions/phi/todo/LICENSE +21 -0
- package/extensions/phi/todo/config.ts +14 -0
- package/extensions/phi/todo/index.ts +113 -0
- package/extensions/phi/todo/locales/de.json +17 -0
- package/extensions/phi/todo/locales/en.json +15 -0
- package/extensions/phi/todo/locales/es.json +17 -0
- package/extensions/phi/todo/locales/fr.json +17 -0
- package/extensions/phi/todo/locales/pt-BR.json +17 -0
- package/extensions/phi/todo/locales/pt.json +17 -0
- package/extensions/phi/todo/locales/ru.json +17 -0
- package/extensions/phi/todo/locales/uk.json +17 -0
- package/extensions/phi/todo/rpiv-config/config.ts +223 -0
- package/extensions/phi/todo/rpiv-config/index.ts +12 -0
- package/extensions/phi/todo/state/i18n-bridge.ts +65 -0
- package/extensions/phi/todo/state/invariants.ts +20 -0
- package/extensions/phi/todo/state/replay.ts +38 -0
- package/extensions/phi/todo/state/selectors.ts +107 -0
- package/extensions/phi/todo/state/state-reducer.ts +187 -0
- package/extensions/phi/todo/state/state.ts +18 -0
- package/extensions/phi/todo/state/store.ts +54 -0
- package/extensions/phi/todo/state/task-graph.ts +57 -0
- package/extensions/phi/todo/todo-overlay.ts +194 -0
- package/extensions/phi/todo/todo.ts +146 -0
- package/extensions/phi/todo/tool/response-envelope.ts +94 -0
- package/extensions/phi/todo/tool/types.ts +128 -0
- package/extensions/phi/todo/view/format.ts +162 -0
- package/package.json +1 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* todo tool + /todos command — thin registration shell.
|
|
3
|
+
*
|
|
4
|
+
* Tool/command identity, schema, types, reducer, store, replay, response
|
|
5
|
+
* envelope, selectors, and view formatters live in the layered modules under
|
|
6
|
+
* `tool/`, `state/`, and `view/`. This file is the package-root registration
|
|
7
|
+
* surface — it mirrors `packages/rpiv-ask-user-question/ask-user-question.ts`
|
|
8
|
+
* which keeps the tool registration at the package root.
|
|
9
|
+
*
|
|
10
|
+
* Public re-exports below preserve the pre-refactor import surface so that
|
|
11
|
+
* `index.ts`, `todo-overlay.ts`, and the global `test/setup.ts` `beforeEach`
|
|
12
|
+
* continue to import from `./todo.js`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ExtensionAPI } from "phi-code";
|
|
16
|
+
import { loadConfig, validateGuidanceFields } from "./config.js";
|
|
17
|
+
import { formatStatusLabel, t } from "./state/i18n-bridge.js";
|
|
18
|
+
import { replayFromBranch } from "./state/replay.js";
|
|
19
|
+
import { selectTasksByStatus, selectTodoCounts, selectVisibleTasks } from "./state/selectors.js";
|
|
20
|
+
import { applyTaskMutation } from "./state/state-reducer.js";
|
|
21
|
+
import { commitState, getState, replaceState } from "./state/store.js";
|
|
22
|
+
import { buildToolResult } from "./tool/response-envelope.js";
|
|
23
|
+
import {
|
|
24
|
+
COMMAND_NAME,
|
|
25
|
+
ERR_REQUIRES_INTERACTIVE,
|
|
26
|
+
MSG_NO_TODOS,
|
|
27
|
+
type TaskMutationParams,
|
|
28
|
+
TOOL_LABEL,
|
|
29
|
+
TOOL_NAME,
|
|
30
|
+
TodoParamsSchema,
|
|
31
|
+
} from "./tool/types.js";
|
|
32
|
+
import { formatCommandTaskLine, renderTodoCall, renderTodoResult } from "./view/format.js";
|
|
33
|
+
|
|
34
|
+
// English fallbacks for localized /todos section headers — the box-drawing
|
|
35
|
+
// decoration is part of the localized string so translators can adjust spacing.
|
|
36
|
+
const SECTION_PENDING = "── Pending ──";
|
|
37
|
+
const SECTION_IN_PROGRESS = "── In Progress ──";
|
|
38
|
+
const SECTION_COMPLETED = "── Completed ──";
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Public re-exports — pre-refactor consumers (overlay, tests, index.ts) keep
|
|
42
|
+
// importing from `./todo.js`. New code may opt into deeper imports.
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export { isTransitionValid } from "./state/invariants.js";
|
|
46
|
+
export { applyTaskMutation } from "./state/state-reducer.js";
|
|
47
|
+
export { __resetState, getNextId, getTodos } from "./state/store.js";
|
|
48
|
+
export { deriveBlocks, detectCycle } from "./state/task-graph.js";
|
|
49
|
+
export type { Task, TaskAction, TaskDetails, TaskStatus } from "./tool/types.js";
|
|
50
|
+
export { TOOL_NAME } from "./tool/types.js";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Backward-compat replay shim. Pre-refactor `reconstructTodoState(ctx)`
|
|
54
|
+
* mutated module state directly; the new replay seam (`state/replay.ts`)
|
|
55
|
+
* returns a `TaskState` and the caller commits via `replaceState`.
|
|
56
|
+
*/
|
|
57
|
+
export function reconstructTodoState(ctx: Parameters<typeof replayFromBranch>[0]): void {
|
|
58
|
+
replaceState(replayFromBranch(ctx));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Tool registration
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export const DEFAULT_PROMPT_SNIPPET = "Manage a task list to track multi-step progress";
|
|
66
|
+
export const DEFAULT_PROMPT_GUIDELINES: string[] = [
|
|
67
|
+
"Use `todo` for complex work with 3+ steps, when the user gives you a list of tasks, or immediately after receiving new instructions to capture requirements. Skip it for single trivial tasks and purely conversational requests.",
|
|
68
|
+
"When starting any task, mark it in_progress BEFORE beginning work. Mark it completed IMMEDIATELY when done — never batch completions. Exactly one task should be in_progress at a time.",
|
|
69
|
+
"Never mark a task completed if tests are failing, the implementation is partial, or you hit unresolved errors — keep it in_progress and create a new task for the blocker instead.",
|
|
70
|
+
"Task status is a 4-state machine: pending → in_progress → completed, plus deleted as a tombstone. Pass activeForm (present-continuous label, e.g. 'researching existing tool') when marking in_progress.",
|
|
71
|
+
"Use blockedBy to express dependencies (A is blocked by B). On create, pass blockedBy as the initial set. On update, use addBlockedBy / removeBlockedBy (additive merge — do not resend the full array). Cycles are rejected.",
|
|
72
|
+
"list hides tombstoned (deleted) tasks by default; pass includeDeleted:true to see them. Pass status to filter by a single status.",
|
|
73
|
+
"Subject must be short and imperative (e.g. 'Research existing tool'); description is for long-form detail. activeForm is a present-continuous label shown while in_progress.",
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
export function registerTodoTool(pi: ExtensionAPI): void {
|
|
77
|
+
const guidance = validateGuidanceFields(loadConfig().guidance);
|
|
78
|
+
pi.registerTool({
|
|
79
|
+
name: TOOL_NAME,
|
|
80
|
+
label: TOOL_LABEL,
|
|
81
|
+
description:
|
|
82
|
+
"Manage a task list for tracking multi-step progress. Actions: create (new task), update (change status/fields/dependencies), list (all tasks, optionally filtered by status), get (single task details), delete (tombstone), clear (reset all). Status: pending → in_progress → completed, plus deleted tombstone. Use this to plan and track multi-step work like research, design, and implementation.",
|
|
83
|
+
promptSnippet: guidance.promptSnippet ?? DEFAULT_PROMPT_SNIPPET,
|
|
84
|
+
promptGuidelines: guidance.promptGuidelines ?? DEFAULT_PROMPT_GUIDELINES,
|
|
85
|
+
parameters: TodoParamsSchema,
|
|
86
|
+
|
|
87
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
88
|
+
const result = applyTaskMutation(getState(), params.action, params as TaskMutationParams);
|
|
89
|
+
commitState(result.state);
|
|
90
|
+
return buildToolResult(params.action, params as TaskMutationParams, result.state, result.op);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
renderCall(args, theme, _context) {
|
|
94
|
+
return renderTodoCall(args as never, theme, getState());
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
renderResult(result, _opts, theme, _context) {
|
|
98
|
+
return renderTodoResult(result, theme);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// /todos slash command
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
export function registerTodosCommand(pi: ExtensionAPI): void {
|
|
108
|
+
pi.registerCommand(COMMAND_NAME, {
|
|
109
|
+
description: "Show all todos on the current branch, grouped by status",
|
|
110
|
+
handler: async (_args, ctx) => {
|
|
111
|
+
if (!ctx.hasUI) {
|
|
112
|
+
ctx.ui.notify(t("command.requires_interactive", ERR_REQUIRES_INTERACTIVE), "error");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const state = getState();
|
|
116
|
+
const visible = selectVisibleTasks(state);
|
|
117
|
+
if (visible.length === 0) {
|
|
118
|
+
ctx.ui.notify(t("command.no_todos", MSG_NO_TODOS), "info");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const groups = selectTasksByStatus(state);
|
|
122
|
+
const counts = selectTodoCounts(state);
|
|
123
|
+
|
|
124
|
+
const header: string[] = [];
|
|
125
|
+
if (counts.completed > 0) header.push(`${counts.completed}/${counts.total} ${formatStatusLabel("completed")}`);
|
|
126
|
+
if (counts.inProgress > 0) header.push(`${counts.inProgress} ${formatStatusLabel("in_progress")}`);
|
|
127
|
+
if (counts.pending > 0) header.push(`${counts.pending} ${formatStatusLabel("pending")}`);
|
|
128
|
+
|
|
129
|
+
const lines: string[] = [header.join(" · ")];
|
|
130
|
+
if (groups.pending.length > 0) {
|
|
131
|
+
lines.push(t("command.section.pending", SECTION_PENDING));
|
|
132
|
+
for (const task of groups.pending) lines.push(formatCommandTaskLine(task, "○"));
|
|
133
|
+
}
|
|
134
|
+
if (groups.inProgress.length > 0) {
|
|
135
|
+
lines.push(t("command.section.in_progress", SECTION_IN_PROGRESS));
|
|
136
|
+
for (const task of groups.inProgress) lines.push(formatCommandTaskLine(task, "◐"));
|
|
137
|
+
}
|
|
138
|
+
if (groups.completed.length > 0) {
|
|
139
|
+
lines.push(t("command.section.completed", SECTION_COMPLETED));
|
|
140
|
+
for (const task of groups.completed) lines.push(formatCommandTaskLine(task, "✓"));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { TaskState } from "../state/state.js";
|
|
2
|
+
import type { Op } from "../state/state-reducer.js";
|
|
3
|
+
import { deriveBlocks } from "../state/task-graph.js";
|
|
4
|
+
import type { Task, TaskAction, TaskDetails, TaskMutationParams } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format a single task as a `[status] #id subject [(activeForm)] [⛓ #dep,…]`
|
|
8
|
+
* line. Used by the `list` content branch only — the overlay and `/todos`
|
|
9
|
+
* formatting paths use `view/format.ts` for richer presentations.
|
|
10
|
+
*/
|
|
11
|
+
function formatListLine(t: Task): string {
|
|
12
|
+
const block = t.blockedBy?.length ? ` ⛓ ${t.blockedBy.map((id) => `#${id}`).join(",")}` : "";
|
|
13
|
+
const form = t.status === "in_progress" && t.activeForm ? ` (${t.activeForm})` : "";
|
|
14
|
+
return `[${t.status}] #${t.id} ${t.subject}${form}${block}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Multi-line presentation for the `get` action. Order of rows is pinned by
|
|
19
|
+
* pre-refactor `todo.ts:354-376` — description, activeForm, blockedBy, blocks,
|
|
20
|
+
* owner — so envelope-level snapshot tests stay byte-equivalent.
|
|
21
|
+
*/
|
|
22
|
+
function formatGetLines(task: Task, state: TaskState): string {
|
|
23
|
+
const blocks = deriveBlocks(state.tasks).get(task.id) ?? [];
|
|
24
|
+
const lines = [`#${task.id} [${task.status}] ${task.subject}`];
|
|
25
|
+
if (task.description) lines.push(` description: ${task.description}`);
|
|
26
|
+
if (task.activeForm) lines.push(` activeForm: ${task.activeForm}`);
|
|
27
|
+
if (task.blockedBy?.length) {
|
|
28
|
+
lines.push(` blockedBy: ${task.blockedBy.map((id) => `#${id}`).join(", ")}`);
|
|
29
|
+
}
|
|
30
|
+
if (blocks.length) {
|
|
31
|
+
lines.push(` blocks: ${blocks.map((id) => `#${id}`).join(", ")}`);
|
|
32
|
+
}
|
|
33
|
+
if (task.owner) lines.push(` owner: ${task.owner}`);
|
|
34
|
+
return lines.join("\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Pure formatter: `(op, state) → string`. Closed switch on `op.kind` —
|
|
39
|
+
* adding a new `Op` variant fails to compile here until a branch is added.
|
|
40
|
+
* The strings on each branch are byte-equivalent to pre-refactor `todo.ts`
|
|
41
|
+
* reducer output.
|
|
42
|
+
*/
|
|
43
|
+
export function formatContent(op: Op, state: TaskState): string {
|
|
44
|
+
switch (op.kind) {
|
|
45
|
+
case "create": {
|
|
46
|
+
const t = state.tasks.find((x) => x.id === op.taskId);
|
|
47
|
+
// Defensive — `op.taskId` always resolves on success path.
|
|
48
|
+
if (!t) return `Created #${op.taskId}`;
|
|
49
|
+
return `Created #${t.id}: ${t.subject} (pending)`;
|
|
50
|
+
}
|
|
51
|
+
case "update": {
|
|
52
|
+
const transition = op.fromStatus !== op.toStatus ? ` (${op.fromStatus} → ${op.toStatus})` : "";
|
|
53
|
+
return `Updated #${op.id}${transition}`;
|
|
54
|
+
}
|
|
55
|
+
case "delete":
|
|
56
|
+
return `Deleted #${op.id}: ${op.subject}`;
|
|
57
|
+
case "clear":
|
|
58
|
+
return `Cleared ${op.count} tasks`;
|
|
59
|
+
case "list": {
|
|
60
|
+
let view = state.tasks;
|
|
61
|
+
if (!op.includeDeleted) view = view.filter((t) => t.status !== "deleted");
|
|
62
|
+
if (op.statusFilter) view = view.filter((t) => t.status === op.statusFilter);
|
|
63
|
+
return view.length === 0 ? "No tasks" : view.map(formatListLine).join("\n");
|
|
64
|
+
}
|
|
65
|
+
case "get":
|
|
66
|
+
return formatGetLines(op.task, state);
|
|
67
|
+
case "error":
|
|
68
|
+
return `Error: ${op.message}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the LLM-facing tool envelope after the store has committed the
|
|
74
|
+
* reducer's new state. `details` is the persistence + replay snapshot —
|
|
75
|
+
* `state/replay.ts` consumes this exact shape on session lifecycle events.
|
|
76
|
+
*
|
|
77
|
+
* Mirrors `packages/rpiv-ask-user-question/tool/response-envelope.ts:13-47`.
|
|
78
|
+
*/
|
|
79
|
+
export function buildToolResult(
|
|
80
|
+
action: TaskAction,
|
|
81
|
+
params: TaskMutationParams,
|
|
82
|
+
state: TaskState,
|
|
83
|
+
op: Op,
|
|
84
|
+
): { content: Array<{ type: "text"; text: string }>; details: TaskDetails } {
|
|
85
|
+
const text = formatContent(op, state);
|
|
86
|
+
const details: TaskDetails = {
|
|
87
|
+
action,
|
|
88
|
+
params: params as Record<string, unknown>,
|
|
89
|
+
tasks: state.tasks,
|
|
90
|
+
nextId: state.nextId,
|
|
91
|
+
...(op.kind === "error" ? { error: op.message } : {}),
|
|
92
|
+
};
|
|
93
|
+
return { content: [{ type: "text", text }], details };
|
|
94
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { StringEnum } from "phi-code-ai";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Tool / command identity — verbatim string boundaries.
|
|
6
|
+
// Tool name "todo" is the persistence key for branch replay (filtering
|
|
7
|
+
// `toolResult.toolName === "todo"`) AND the permissions entry at
|
|
8
|
+
// `templates/pi-permissions.jsonc:26`. DO NOT rename.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export const TOOL_NAME = "todo";
|
|
12
|
+
export const TOOL_LABEL = "Todo";
|
|
13
|
+
export const COMMAND_NAME = "todos";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// User-facing strings (kept stable for /todos UX parity).
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export const ERR_REQUIRES_INTERACTIVE = "/todos requires interactive mode";
|
|
20
|
+
export const MSG_NO_TODOS = "No todos yet. Ask the agent to add some!";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Public domain types
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export type TaskStatus = "pending" | "in_progress" | "completed" | "deleted";
|
|
27
|
+
|
|
28
|
+
export type TaskAction = "create" | "update" | "list" | "get" | "delete" | "clear";
|
|
29
|
+
|
|
30
|
+
export interface Task {
|
|
31
|
+
id: number;
|
|
32
|
+
subject: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
activeForm?: string;
|
|
35
|
+
status: TaskStatus;
|
|
36
|
+
blockedBy?: number[];
|
|
37
|
+
owner?: string;
|
|
38
|
+
metadata?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Persistence + replay snapshot. Every successful `todo` tool call returns this
|
|
43
|
+
* shape under `details`; `state/replay.ts` reads the latest one from the branch
|
|
44
|
+
* to reconstruct module state. Field order and field names are pinned by
|
|
45
|
+
* cross-version replay compatibility.
|
|
46
|
+
*/
|
|
47
|
+
export interface TaskDetails {
|
|
48
|
+
action: TaskAction;
|
|
49
|
+
params: Record<string, unknown>;
|
|
50
|
+
tasks: Task[];
|
|
51
|
+
nextId: number;
|
|
52
|
+
error?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Open-shape input bag the reducer accepts. Stays an interface so the index
|
|
57
|
+
* signature (`[key: string]: unknown`) lets the runtime pass through TypeBox
|
|
58
|
+
* `Static<typeof TodoParamsSchema>` without `as` casts.
|
|
59
|
+
*/
|
|
60
|
+
export interface TaskMutationParams {
|
|
61
|
+
[key: string]: unknown;
|
|
62
|
+
subject?: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
activeForm?: string;
|
|
65
|
+
status?: TaskStatus;
|
|
66
|
+
blockedBy?: number[];
|
|
67
|
+
addBlockedBy?: number[];
|
|
68
|
+
removeBlockedBy?: number[];
|
|
69
|
+
owner?: string;
|
|
70
|
+
metadata?: Record<string, unknown>;
|
|
71
|
+
id?: number;
|
|
72
|
+
includeDeleted?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// TypeBox parameter schema — every `description` doubles as LLM-facing prompt
|
|
77
|
+
// copy. Field order and wording are pinned by registration tests and the
|
|
78
|
+
// pre-refactor schema at `packages/rpiv-todo/todo.ts:512-573`.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
export const TodoParamsSchema = Type.Object({
|
|
82
|
+
action: StringEnum(["create", "update", "list", "get", "delete", "clear"] as const),
|
|
83
|
+
subject: Type.Optional(Type.String({ description: "Task subject line (required for create)" })),
|
|
84
|
+
description: Type.Optional(Type.String({ description: "Long-form task description" })),
|
|
85
|
+
activeForm: Type.Optional(
|
|
86
|
+
Type.String({
|
|
87
|
+
description: "Present-continuous spinner label shown while status is in_progress (e.g. 'writing tests')",
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
status: Type.Optional(
|
|
91
|
+
StringEnum(["pending", "in_progress", "completed", "deleted"] as const, {
|
|
92
|
+
description: "Target status (update) or list filter (list)",
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
blockedBy: Type.Optional(
|
|
96
|
+
Type.Array(Type.Number(), {
|
|
97
|
+
description: "Initial blockedBy ids (create only)",
|
|
98
|
+
}),
|
|
99
|
+
),
|
|
100
|
+
addBlockedBy: Type.Optional(
|
|
101
|
+
Type.Array(Type.Number(), {
|
|
102
|
+
description: "Task ids to add to blockedBy (update only, additive merge)",
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
removeBlockedBy: Type.Optional(
|
|
106
|
+
Type.Array(Type.Number(), {
|
|
107
|
+
description: "Task ids to remove from blockedBy (update only, additive merge)",
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
owner: Type.Optional(Type.String({ description: "Agent/owner assigned to this task" })),
|
|
111
|
+
metadata: Type.Optional(
|
|
112
|
+
Type.Record(Type.String(), Type.Unknown(), {
|
|
113
|
+
description: "Arbitrary metadata; pass null value for a key to delete that key on update",
|
|
114
|
+
}),
|
|
115
|
+
),
|
|
116
|
+
id: Type.Optional(
|
|
117
|
+
Type.Number({
|
|
118
|
+
description: "Task id (required for update, get, delete)",
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
includeDeleted: Type.Optional(
|
|
122
|
+
Type.Boolean({
|
|
123
|
+
description: "If true, list action returns deleted (tombstoned) tasks as well. Default: false.",
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export type TodoParams = Static<typeof TodoParamsSchema>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Theme } from "phi-code";
|
|
2
|
+
import { Text } from "phi-code-tui";
|
|
3
|
+
import { formatStatusLabel } from "../state/i18n-bridge.js";
|
|
4
|
+
import { selectTaskSubjectById } from "../state/selectors.js";
|
|
5
|
+
import type { TaskState } from "../state/state.js";
|
|
6
|
+
import type { Task, TaskAction, TaskDetails, TaskMutationParams, TaskStatus } from "../tool/types.js";
|
|
7
|
+
|
|
8
|
+
// Re-export so legacy import paths (todo.ts, tests) continue to resolve;
|
|
9
|
+
// the canonical definition lives in the i18n bridge.
|
|
10
|
+
export { formatStatusLabel };
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Status presentation tables — the single source of truth for glyph/color.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export const STATUS_GLYPH: Record<TaskStatus, string> = {
|
|
17
|
+
pending: "○",
|
|
18
|
+
in_progress: "◐",
|
|
19
|
+
completed: "●",
|
|
20
|
+
deleted: "⊘",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Color palette for the renderResult status echo. `deleted` uses `muted` so a
|
|
25
|
+
* successful delete is visually distinct from the error branch (which uses
|
|
26
|
+
* `error` + `✗`). Mirrors pre-refactor `todo.ts:444-450`.
|
|
27
|
+
*/
|
|
28
|
+
export const STATUS_COLOR: Record<TaskStatus, "dim" | "warning" | "success" | "muted"> = {
|
|
29
|
+
pending: "dim",
|
|
30
|
+
in_progress: "warning",
|
|
31
|
+
completed: "success",
|
|
32
|
+
deleted: "muted",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Per-action prefix glyph for renderCall. `+` create, `→` update, `×` delete,
|
|
37
|
+
* `›` get, `☰` list, `∅` clear. Pre-refactor `todo.ts:457-464`.
|
|
38
|
+
*/
|
|
39
|
+
export const ACTION_GLYPH: Record<TaskAction, string> = {
|
|
40
|
+
create: "+",
|
|
41
|
+
update: "→",
|
|
42
|
+
delete: "×",
|
|
43
|
+
get: "›",
|
|
44
|
+
list: "☰",
|
|
45
|
+
clear: "∅",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Glyph for the persistent overlay's per-task row. Differs from `STATUS_GLYPH`
|
|
50
|
+
* for `completed` (`✓` vs `●`) and `deleted` (`✗` vs `⊘`) because the
|
|
51
|
+
* overlay caller never renders a `deleted` row but uses `✗` in its
|
|
52
|
+
* error-toned palette. Mirrors pre-refactor `todo-overlay.ts:23-33`.
|
|
53
|
+
*/
|
|
54
|
+
export function overlayStatusGlyph(status: TaskStatus, theme: Theme): string {
|
|
55
|
+
switch (status) {
|
|
56
|
+
case "pending":
|
|
57
|
+
return theme.fg("dim", "○");
|
|
58
|
+
case "in_progress":
|
|
59
|
+
return theme.fg("warning", "◐");
|
|
60
|
+
case "completed":
|
|
61
|
+
return theme.fg("success", "✓");
|
|
62
|
+
case "deleted":
|
|
63
|
+
return theme.fg("error", "✗");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format a single task for the overlay (with theme + glyph + dep suffix).
|
|
69
|
+
* Used by `TodoOverlay.formatTaskLine` post-refactor; behavior is unchanged.
|
|
70
|
+
*/
|
|
71
|
+
export function formatOverlayTaskLine(t: Task, theme: Theme, showId: boolean): string {
|
|
72
|
+
const glyph = overlayStatusGlyph(t.status, theme);
|
|
73
|
+
const subjectColor = t.status === "completed" || t.status === "deleted" ? "dim" : "text";
|
|
74
|
+
let subject = theme.fg(subjectColor, t.subject);
|
|
75
|
+
if (t.status === "completed" || t.status === "deleted") {
|
|
76
|
+
subject = theme.strikethrough(subject);
|
|
77
|
+
}
|
|
78
|
+
let line = `${glyph}`;
|
|
79
|
+
if (showId) line += ` ${theme.fg("accent", `#${t.id}`)}`;
|
|
80
|
+
line += ` ${subject}`;
|
|
81
|
+
if (t.status === "in_progress" && t.activeForm) {
|
|
82
|
+
line += ` ${theme.fg("dim", `(${t.activeForm})`)}`;
|
|
83
|
+
}
|
|
84
|
+
if (t.blockedBy && t.blockedBy.length > 0) {
|
|
85
|
+
line += ` ${theme.fg("dim", `⛓ ${t.blockedBy.map((id) => `#${id}`).join(",")}`)}`;
|
|
86
|
+
}
|
|
87
|
+
return line;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format a single task line for the `/todos` slash command (no glyph color,
|
|
92
|
+
* indented bullet prefix). Pre-refactor `todo.ts:670-674`.
|
|
93
|
+
*/
|
|
94
|
+
export function formatCommandTaskLine(t: Task, glyph: string): string {
|
|
95
|
+
const form = t.status === "in_progress" && t.activeForm ? ` (${t.activeForm})` : "";
|
|
96
|
+
const block = t.blockedBy?.length ? ` ⛓ ${t.blockedBy.map((id) => `#${id}`).join(",")}` : "";
|
|
97
|
+
return ` ${glyph} #${t.id} ${t.subject}${form}${block}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Tool render hooks — wrapped so `todo.ts` becomes a thin call-site.
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* `renderCall` body. Receives the parsed args, the theme, and the live
|
|
106
|
+
* `TaskState` (resolved by the caller via `getState()`). Returns a `Text`
|
|
107
|
+
* node identical to pre-refactor `todo.ts:507-525`.
|
|
108
|
+
*/
|
|
109
|
+
export function renderTodoCall(
|
|
110
|
+
args: TaskMutationParams & { action: TaskAction },
|
|
111
|
+
theme: Theme,
|
|
112
|
+
state: TaskState,
|
|
113
|
+
): Text {
|
|
114
|
+
const glyph = ACTION_GLYPH[args.action] ?? args.action;
|
|
115
|
+
let text = theme.fg("toolTitle", theme.bold("todo ")) + theme.fg("muted", glyph);
|
|
116
|
+
|
|
117
|
+
if (args.action === "create" && args.subject) {
|
|
118
|
+
text += ` ${theme.fg("dim", args.subject)}`;
|
|
119
|
+
} else if (
|
|
120
|
+
(args.action === "update" || args.action === "get" || args.action === "delete") &&
|
|
121
|
+
args.id !== undefined
|
|
122
|
+
) {
|
|
123
|
+
const subject = selectTaskSubjectById(state, args.id);
|
|
124
|
+
text += ` ${theme.fg("accent", subject ?? `#${args.id}`)}`;
|
|
125
|
+
} else if (args.action === "list" && args.status) {
|
|
126
|
+
text += ` ${theme.fg("muted", formatStatusLabel(args.status))}`;
|
|
127
|
+
}
|
|
128
|
+
return new Text(text, 0, 0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `renderResult` body. Inspects `details` to pick the per-action status echo
|
|
133
|
+
* (only `create`/`update`/`delete` advertise a status; `list`/`get`/`clear`
|
|
134
|
+
* fall back to plain `✓`). Identical visual output to pre-refactor
|
|
135
|
+
* `todo.ts:533-565`.
|
|
136
|
+
*/
|
|
137
|
+
export function renderTodoResult(result: { details?: unknown }, theme: Theme): Text {
|
|
138
|
+
const details = result.details as TaskDetails | undefined;
|
|
139
|
+
let status: TaskStatus | undefined;
|
|
140
|
+
if (details) {
|
|
141
|
+
const params = details.params as TaskMutationParams;
|
|
142
|
+
switch (details.action) {
|
|
143
|
+
case "create":
|
|
144
|
+
status = details.tasks[details.tasks.length - 1]?.status;
|
|
145
|
+
break;
|
|
146
|
+
case "update":
|
|
147
|
+
status = params.status ?? details.tasks.find((t) => t.id === params.id)?.status;
|
|
148
|
+
break;
|
|
149
|
+
case "delete":
|
|
150
|
+
status = details.tasks.find((t) => t.id === params.id)?.status;
|
|
151
|
+
break;
|
|
152
|
+
case "list":
|
|
153
|
+
case "get":
|
|
154
|
+
case "clear":
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (status) {
|
|
159
|
+
return new Text(theme.fg(STATUS_COLOR[status], `${STATUS_GLYPH[status]} ${formatStatusLabel(status)}`), 0, 0);
|
|
160
|
+
}
|
|
161
|
+
return new Text(theme.fg("success", "✓"), 0, 0);
|
|
162
|
+
}
|