pi-feats 0.1.4 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +90 -9
  2. package/extensions/api-server/README.md +21 -1
  3. package/extensions/api-server/application-log-store.ts +4 -2
  4. package/extensions/api-server/application-runtime.ts +79 -8
  5. package/extensions/api-server/application-store.ts +23 -10
  6. package/extensions/api-server/index.ts +6 -2
  7. package/extensions/api-server/profile-store.ts +109 -39
  8. package/extensions/api-server/server.ts +220 -44
  9. package/extensions/cli-resources.ts +144 -113
  10. package/extensions/cli-tui-mode.ts +7 -0
  11. package/extensions/context-memory/index.ts +6 -1
  12. package/extensions/conversation-search/index.ts +42 -0
  13. package/extensions/guardrails/index.ts +2 -1
  14. package/extensions/lib/application-context.ts +2 -0
  15. package/extensions/lib/builtin-tools.ts +24 -0
  16. package/extensions/lib/cli-tui-mode.ts +72 -0
  17. package/extensions/lib/context-memory.ts +6 -1
  18. package/extensions/lib/conversation-search.ts +33 -0
  19. package/extensions/lib/package-resources.ts +58 -0
  20. package/extensions/lib/pi-command.ts +11 -0
  21. package/extensions/lib/profile-sandbox.ts +14 -3
  22. package/extensions/lib/remote-hosts.ts +7 -5
  23. package/extensions/lib/workflow-evaluator.ts +113 -0
  24. package/extensions/lib/workflow-harness.ts +409 -43
  25. package/extensions/pi-console-webui/app/[section]/page.tsx +1 -1
  26. package/extensions/pi-console-webui/app/api/admin/models/route.ts +22 -0
  27. package/extensions/pi-console-webui/app/globals.css +3 -0
  28. package/extensions/pi-console-webui/components/application-handler-editor.tsx +1 -1
  29. package/extensions/pi-console-webui/components/application-logs.tsx +4 -4
  30. package/extensions/pi-console-webui/components/application-mappings.tsx +10 -12
  31. package/extensions/pi-console-webui/components/application-sessions.tsx +2 -2
  32. package/extensions/pi-console-webui/components/application-settings.tsx +12 -6
  33. package/extensions/pi-console-webui/components/chat-workspace.tsx +17 -7
  34. package/extensions/pi-console-webui/components/console-page.tsx +4 -0
  35. package/extensions/pi-console-webui/components/console-state.tsx +1 -1
  36. package/extensions/pi-console-webui/components/console.tsx +32 -11
  37. package/extensions/pi-console-webui/components/guardrails-panel.tsx +2 -2
  38. package/extensions/pi-console-webui/components/models-config-form.tsx +31 -0
  39. package/extensions/pi-console-webui/components/profile-model-form.tsx +54 -0
  40. package/extensions/pi-console-webui/components/pulse-resources.tsx +7 -6
  41. package/extensions/pi-console-webui/components/sequential-workflow-config.tsx +32 -0
  42. package/extensions/pi-console-webui/components/skill-resources.tsx +4 -3
  43. package/extensions/pi-console-webui/components/ui/input.tsx +1 -1
  44. package/extensions/pi-console-webui/index.ts +2 -1
  45. package/extensions/pi-console-webui/lib/admin-models.ts +38 -0
  46. package/extensions/pi-console-webui/lib/page-metadata.ts +4 -1
  47. package/extensions/pi-console-webui/next.config.js +1 -0
  48. package/extensions/profile-ssh.ts +274 -0
  49. package/extensions/profiles.ts +69 -25
  50. package/extensions/pulse/index.ts +148 -16
  51. package/extensions/pulse/store.ts +132 -18
  52. package/extensions/sequential-workflow.ts +121 -79
  53. package/extensions/skill-sources/store.ts +29 -10
  54. package/package.json +21 -9
  55. package/scripts/build-console-webui.mjs +7 -0
  56. package/scripts/console-webui-cache.mjs +79 -0
  57. package/scripts/ensure-console-build.mjs +43 -0
  58. package/skills/profile-ssh/SKILL.md +65 -0
  59. package/skills/sequential-workflow/SKILL.md +4 -3
  60. package/themes/legacy-dark.json +85 -0
@@ -1,6 +1,11 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { DatabaseSync } from "node:sqlite";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
+ import { readFile, readdir } from "node:fs/promises";
5
+ import { homedir } from "node:os";
6
+ import { basename, join, resolve } from "node:path";
7
+ import { evaluateWorkflowRequirement, evaluateWorkflowTask, workflowEvaluationConfig, type RequirementDecision } from "./workflow-evaluator.ts";
8
+ import { Box, Text } from "@earendil-works/pi-tui";
4
9
  /** Session ownership and execution gates. Prompts explain state; these gates enforce it. */
5
10
  export function workflowHarness(host: ExtensionAPI, db: DatabaseSync) {
6
11
  db.exec("PRAGMA busy_timeout = 5000");
@@ -9,7 +14,21 @@ export function workflowHarness(host: ExtensionAPI, db: DatabaseSync) {
9
14
  }
10
15
  db.exec(`CREATE INDEX IF NOT EXISTS workflow_session_idx ON workflows(session_id);
11
16
  CREATE TABLE IF NOT EXISTS workflow_focus (session_id TEXT PRIMARY KEY, stack TEXT NOT NULL);
12
- CREATE TABLE IF NOT EXISTS workflow_collect_checkpoint (task_id INTEGER PRIMARY KEY REFERENCES workflow_tasks(id), user_entry_id TEXT);`);
17
+ CREATE TABLE IF NOT EXISTS workflow_collect_checkpoint (task_id INTEGER PRIMARY KEY REFERENCES workflow_tasks(id), user_entry_id TEXT);
18
+ CREATE TABLE IF NOT EXISTS workflow_harness_events (
19
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
20
+ session_id TEXT NOT NULL,
21
+ workflow_id INTEGER REFERENCES workflows(id),
22
+ phase TEXT NOT NULL,
23
+ origin TEXT,
24
+ payload TEXT NOT NULL,
25
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
26
+ );
27
+ CREATE TABLE IF NOT EXISTS workflow_requirement_blocks (
28
+ session_id TEXT PRIMARY KEY,
29
+ reason TEXT NOT NULL,
30
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
31
+ );`);
13
32
  const context = new AsyncLocalStorage<ExtensionContext>();
14
33
  const session = () => {
15
34
  const id = context.getStore()?.sessionManager.getSessionId();
@@ -19,7 +38,7 @@ export function workflowHarness(host: ExtensionAPI, db: DatabaseSync) {
19
38
  };
20
39
  const row = (id: number) => db.prepare("SELECT * FROM workflows WHERE id = ?").get(id) as any;
21
40
  const task = (id: number) => db.prepare("SELECT * FROM workflow_tasks WHERE workflow_id = ? AND status NOT IN ('accepted', 'failed') ORDER BY position LIMIT 1").get(id) as any;
22
- const active = (w: any) => w && ["running", "awaiting_user", "evaluating"].includes(w.status);
41
+ const active = (w: any) => w && ["pending_definition", "running", "awaiting_user", "evaluating"].includes(w.status);
23
42
  const save = (stack: number[]) => db.prepare("INSERT INTO workflow_focus VALUES (?, ?) ON CONFLICT(session_id) DO UPDATE SET stack=excluded.stack").run(session(), JSON.stringify(stack));
24
43
  const stack = (): number[] => {
25
44
  const saved = db.prepare("SELECT stack FROM workflow_focus WHERE session_id = ?").get(session()) as any;
@@ -29,6 +48,14 @@ export function workflowHarness(host: ExtensionAPI, db: DatabaseSync) {
29
48
  });
30
49
  };
31
50
  const focus = () => stack().at(-1);
51
+ const requirementBlock = () => db.prepare("SELECT reason FROM workflow_requirement_blocks WHERE session_id = ?").get(session()) as { reason: string } | undefined;
52
+ const clearRequirementBlock = () => db.prepare("DELETE FROM workflow_requirement_blocks WHERE session_id = ?").run(session());
53
+ const blockRequirement = (reason: string) => db.prepare("INSERT INTO workflow_requirement_blocks(session_id, reason) VALUES (?, ?) ON CONFLICT(session_id) DO UPDATE SET reason = excluded.reason, created_at = CURRENT_TIMESTAMP").run(session(), reason);
54
+ const pendingWorkflow = () => {
55
+ const id = focus();
56
+ const item = id ? row(id) : undefined;
57
+ return item?.status === "pending_definition" ? id : undefined;
58
+ };
32
59
  const own = (id: number) => {
33
60
  const w = row(id);
34
61
  if (!w || w.session_id !== session())
@@ -36,6 +63,87 @@ export function workflowHarness(host: ExtensionAPI, db: DatabaseSync) {
36
63
  return w;
37
64
  };
38
65
  const userEntry = () => context.getStore()!.sessionManager.getBranch().filter((e: any) => e.type === "message" && e.message.role === "user").at(-1)?.id ?? null;
66
+ const addEvent = (workflowId: number, phase: string, payload: unknown) => db.prepare("INSERT INTO workflow_events(workflow_id, phase, payload) VALUES (?, ?, ?) ").run(workflowId, phase, JSON.stringify(payload));
67
+ const audit = (phase: string, payload: unknown, origin?: "user_input" | "skill", workflowId?: number, display = false) => {
68
+ db.prepare("INSERT INTO workflow_harness_events(session_id, workflow_id, phase, origin, payload) VALUES (?, ?, ?, ?, ?)").run(session(), workflowId ?? null, phase, origin ?? null, JSON.stringify(payload));
69
+ if (workflowId)
70
+ addEvent(workflowId, `harness_${phase}`, payload);
71
+ if (display)
72
+ host.sendMessage({ customType: "sequential-workflow-harness", display: true, content: (payload as any).message ?? phase, details: { phase, origin, workflowId, ...(payload as any) } });
73
+ };
74
+ const blockEvaluation = (reason: string, payload: Record<string, unknown> = {}, origin?: "user_input" | "skill", workflowId?: number) => {
75
+ blockRequirement(reason);
76
+ audit("evaluation_blocked", { ...payload, reason, message: "Sequential Workflow decision is unresolved; work remains blocked." }, origin, workflowId, true);
77
+ };
78
+ type DefinitionActivation = "definition" | "template";
79
+ const definitionTools = (activation: DefinitionActivation) => activation === "template"
80
+ ? ["read", "sequential_workflow_create_from_template"]
81
+ : ["sequential_workflow_create"];
82
+ const definitionCreationTool = (activation: DefinitionActivation) => activation === "template"
83
+ ? "sequential_workflow_create_from_template"
84
+ : "sequential_workflow_create";
85
+ const definitionMode = (workflowId = pendingWorkflow()) => {
86
+ if (!workflowId)
87
+ return undefined;
88
+ const event = db.prepare("SELECT payload FROM workflow_events WHERE workflow_id = ? AND phase = 'definition_mode_started' ORDER BY id DESC LIMIT 1").get(workflowId) as any;
89
+ if (!event)
90
+ return undefined;
91
+ try {
92
+ const mode = JSON.parse(event.payload) as { origin: "user_input" | "skill"; userRequest: string; skillPath?: string; activeTools: string[]; activation?: DefinitionActivation; templatePath?: string };
93
+ return { ...mode, activation: mode.activation ?? "definition" };
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ };
99
+ const restoreDefinitionTools = (workflowId: number) => {
100
+ const mode = definitionMode(workflowId);
101
+ if (mode?.activeTools?.length)
102
+ host.setActiveTools(mode.activeTools);
103
+ };
104
+ const startDefinitionMode = (workflowId: number, origin: "user_input" | "skill", userRequest: string, activation: DefinitionActivation = "definition", templatePath?: string, skillPath?: string) => {
105
+ const activeTools = host.getActiveTools();
106
+ addEvent(workflowId, "definition_mode_started", { origin, userRequest, activation, templatePath, skillPath, activeTools });
107
+ host.setActiveTools(definitionTools(activation));
108
+ };
109
+ const definitionEvaluationSent = (workflowId: number) => Boolean(db.prepare("SELECT 1 FROM workflow_events WHERE workflow_id = ? AND phase = 'definition_mode_evaluated' LIMIT 1").get(workflowId));
110
+ const harnessEventExists = (workflowId: number, phase: string) => Boolean(db.prepare("SELECT 1 FROM workflow_harness_events WHERE session_id = ? AND (workflow_id = ? OR workflow_id IS NULL) AND phase = ? LIMIT 1").get(session(), workflowId, phase));
111
+ const definitionAnnouncementSent = (workflowId: number) => Boolean(db.prepare("SELECT 1 FROM workflow_events WHERE workflow_id = ? AND phase = 'definition_mode_announced' LIMIT 1").get(workflowId));
112
+ const announceDefinitionMode = (workflowId: number) => {
113
+ const mode = definitionMode(workflowId);
114
+ if (!mode || definitionAnnouncementSent(workflowId))
115
+ return undefined;
116
+ const template = mode.activation === "template";
117
+ addEvent(workflowId, "definition_mode_announced", { activation: mode.activation, templatePath: mode.templatePath });
118
+ const content = template
119
+ ? mode.origin === "skill"
120
+ ? `Preparing workflow #${workflowId} from the template specified by the required Skill.`
121
+ : `Preparing workflow #${workflowId} from template: ${mode.templatePath}`
122
+ : `Workflow #${workflowId} created — defining ordered tasks.`;
123
+ return { customType: "sequential-workflow-created", content, display: true, details: { workflowId, origin: mode.origin, activation: mode.activation, templatePath: mode.templatePath } };
124
+ };
125
+ const createPendingWorkflow = (origin: "user_input" | "skill", evidence: string, reasoning: string) => {
126
+ const existing = pendingWorkflow();
127
+ if (existing)
128
+ return existing;
129
+ const inserted = db.prepare("INSERT INTO workflows(title, source, status, session_id) VALUES (?, ?, 'pending_definition', ?)").run("Pending workflow definition", `Sequential Workflow requirement detected from ${origin}.`, session());
130
+ const workflowId = Number(inserted.lastInsertRowid);
131
+ audit("requirement_detected", { origin, evidence, message: `Sequential Workflow requirement detected from ${origin === "skill" ? "a required Skill" : "the user request"}.` }, origin, workflowId);
132
+ audit("requirement_classified_required", { requiresSequentialWorkflow: true, reasoning, message: "Sequential Workflow is required for this request." }, origin, workflowId);
133
+ save([...stack(), workflowId]);
134
+ return workflowId;
135
+ };
136
+ const cancelPendingWorkflowByUser = (reason: string) => {
137
+ const id = pendingWorkflow();
138
+ if (!id)
139
+ return false;
140
+ restoreDefinitionTools(id);
141
+ db.prepare("UPDATE workflows SET status='cancelled', updated_at=CURRENT_TIMESTAMP WHERE id=?").run(id);
142
+ addEvent(id, "cancelled_by_user", { reason });
143
+ save(stack().filter((workflowId) => workflowId !== id));
144
+ return true;
145
+ };
146
+ const explicitUserOverride = (text: string) => /\b(?:skip|ignore|cancel|end|stop|do not (?:use|create)|don't (?:use|create)|continue manually|manual(?:mente)?|cancelar|encerrar|parar|ignorar|n[aã]o (?:use|crie)|sem (?:usar|criar)|seguir manualmente)\b/iu.test(text);
39
147
  const checkpoint = () => {
40
148
  const id = focus();
41
149
  const t = id ? task(id) : undefined;
@@ -61,12 +169,42 @@ export function workflowHarness(host: ExtensionAPI, db: DatabaseSync) {
61
169
  queue = next.catch(() => undefined);
62
170
  return next;
63
171
  };
64
- let namedFailure = false;
172
+ const templateCandidates = async () => {
173
+ const directory = join(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "sequential_workflow_templates");
174
+ try {
175
+ return (await readdir(directory, { withFileTypes: true })).filter(entry => entry.isFile() && entry.name.endsWith(".json")).map(entry => ({ id: entry.name.slice(0, -5), title: entry.name.slice(0, -5), source: join(directory, entry.name) }));
176
+ } catch { return []; }
177
+ };
178
+ const messageText = (content: unknown) => typeof content === "string" ? content : Array.isArray(content) ? content.map((part: any) => part.text ?? "").join("") : "";
179
+ const classifyRequirement = async (ctx: ExtensionContext, origin: "user_input" | "skill", evidence: string, userRequest: string): Promise<RequirementDecision> => {
180
+ const templates = await templateCandidates();
181
+ const branch = ctx.sessionManager.getBranch();
182
+ if (origin === "user_input") {
183
+ const conversationHistory = branch
184
+ .filter((entry: any) => entry.type === "message" && (entry.message?.role === "user" || entry.message?.role === "assistant"))
185
+ .map((entry: any) => ({ role: entry.message.role as "user" | "assistant", content: messageText(entry.message.content) }))
186
+ .filter((entry: { content: string }) => !!entry.content.trim())
187
+ .slice(-12);
188
+ // Depending on Pi's event ordering, the current message may already be
189
+ // in the branch. It belongs only in userLastMessage, never twice.
190
+ const last = conversationHistory.at(-1);
191
+ if (last?.role === "user" && last.content === userRequest)
192
+ conversationHistory.pop();
193
+ return evaluateWorkflowRequirement(ctx, { origin, userLastMessage: userRequest, conversationHistory, templates });
194
+ }
195
+ return evaluateWorkflowRequirement(ctx, {
196
+ origin,
197
+ evidence,
198
+ userRequest,
199
+ recentUserMessages: branch.filter((entry: any) => entry.type === "message" && entry.message?.role === "user").slice(-4).map((entry: any) => messageText(entry.message.content)),
200
+ templates,
201
+ });
202
+ };
65
203
  const assertOperation = (name: string, params: any) => {
66
- if (namedFailure && (name === "sequential_workflow_create" || name.endsWith("create_from_template")))
67
- throw new Error("Named activation failed; a new user request is required before creating a replacement.");
68
204
  if (params.workflowId !== undefined && name !== "sequential_workflow_cancel")
69
205
  own(params.workflowId);
206
+ if (name.endsWith("evaluate") && requirementBlock())
207
+ throw new Error("Configured workflow evaluation is unresolved; manual evaluation is blocked.");
70
208
  if (name.endsWith("record_result") || name.endsWith("evaluate")) {
71
209
  if (focus() !== params.workflowId)
72
210
  throw new Error("Only the focused workflow can advance. Use /workflow-focus explicitly.");
@@ -80,29 +218,128 @@ export function workflowHarness(host: ExtensionAPI, db: DatabaseSync) {
80
218
  }
81
219
  }
82
220
  if (name === "sequential_workflow_create" || name.endsWith("create_from_template")) {
83
- if (params.parentWorkflowId !== undefined)
84
- own(params.parentWorkflowId);
85
- if (params.parentTaskId !== undefined) {
86
- const t = db.prepare("SELECT * FROM workflow_tasks WHERE id = ?").get(params.parentTaskId) as any;
87
- if (!t || own(t.workflow_id).id !== focus() || task(t.workflow_id)?.id !== t.id)
88
- throw new Error("Child must belong to the current focused task.");
89
- }
90
- else if (focus() !== undefined)
221
+ const pending = pendingWorkflow();
222
+ const mode = definitionMode(pending);
223
+ if (pending && mode && name !== definitionCreationTool(mode.activation))
224
+ throw new Error(`Definition mode requires ${definitionCreationTool(mode.activation)}.`);
225
+ if (focus() !== undefined && pendingWorkflow() === undefined)
91
226
  throw new Error("Suspend the current workflow with /workflow-suspend before creating an independent execution.");
92
227
  }
228
+ if (name === "sequential_workflow_create_subworkflow") {
229
+ const t = db.prepare("SELECT * FROM workflow_tasks WHERE id = ?").get(params.parentTaskId) as any;
230
+ if (!t || t.workflow_id !== params.parentWorkflowId || own(t.workflow_id).id !== focus() || task(t.workflow_id)?.id !== t.id)
231
+ throw new Error("A subworkflow must belong to the current focused task.");
232
+ if (t.type !== "workflow" || t.status !== "running")
233
+ throw new Error("A subworkflow requires a running workflow task.");
234
+ }
93
235
  };
236
+ host.registerMessageRenderer("sequential-workflow-evaluation", (message, { outputPad }, theme) => {
237
+ const box = new Box(outputPad, 0);
238
+ box.addChild(new Text(`${theme.fg("muted", "Evaluating Sequential Workflow requirement")}\n${theme.fg("dim", String(message.content))}`, 0, 0));
239
+ return box;
240
+ });
241
+ host.registerMessageRenderer("sequential-workflow-created", (message, { outputPad }, theme) => {
242
+ const workflowId = (message.details as any)?.workflowId;
243
+ const box = new Box(outputPad, 0);
244
+ box.addChild(new Text(`${theme.fg("muted", "Planning Sequential Workflow")}${workflowId ? ` #${workflowId}` : ""}\n${theme.fg("dim", String(message.content))}`, 0, 0));
245
+ return box;
246
+ });
247
+ host.registerMessageRenderer("sequential-workflow-harness", (message, { outputPad }, theme) => {
248
+ const details = (message.details ?? {}) as { phase?: string; workflowId?: number; taskId?: number; taskPosition?: number; evaluator?: string; model?: string; reasoning?: string };
249
+ const workflow = details.workflowId ? `Workflow #${details.workflowId}` : "Sequential Workflow";
250
+ const task = details.taskPosition ? ` · Task #${details.taskPosition}` : "";
251
+ const phase = details.phase ?? "";
252
+ const presentation: Record<string, { title: string; color: "accent" | "success" | "warning" | "error" | "muted" }> = {
253
+ requirement_check_started: { title: "Evaluating requirement", color: "accent" },
254
+ requirement_classified_required: { title: "Workflow required", color: "accent" },
255
+ requirement_classified_not_required: { title: "Workflow not required", color: "success" },
256
+ requirement_classified_uncertain: { title: "Decision uncertain", color: "warning" },
257
+ requirement_classified_unavailable: { title: "Evaluator unavailable", color: "error" },
258
+ requirement_classified_invalid_response: { title: "Invalid evaluator response", color: "error" },
259
+ requirement_classified_error: { title: "Requirement evaluation failed", color: "error" },
260
+ evaluation_blocked: { title: "Work blocked", color: "error" },
261
+ definition_mode_started: { title: "Defining workflow", color: "accent" },
262
+ workflow_hydrated: { title: "Workflow ready", color: "success" },
263
+ task_evaluation_accept: { title: "Task accepted", color: "success" },
264
+ task_evaluation_retry: { title: "Task will be retried", color: "warning" },
265
+ task_evaluation_fail: { title: "Terminal failure", color: "error" },
266
+ task_evaluation_uncertain: { title: "Task evaluation uncertain", color: "warning" },
267
+ task_evaluation_unavailable: { title: "Task evaluator unavailable", color: "error" },
268
+ task_evaluation_invalid_response: { title: "Invalid task response", color: "error" },
269
+ task_evaluation_error: { title: "Task evaluation failed", color: "error" },
270
+ workflow_completed: { title: "Workflow completed", color: "success" },
271
+ workflow_cancelled: { title: "Workflow cancelled", color: "warning" },
272
+ };
273
+ const current = presentation[phase] ?? { title: "Workflow update", color: "muted" as const };
274
+ const header = `${workflow}${task} · ${current.title}`;
275
+ const metadata = [details.evaluator, details.model].filter(Boolean).join(" · ");
276
+ const body = [theme.fg(current.color, header), theme.fg("dim", String(message.content)), metadata ? theme.fg("muted", `Evaluator: ${metadata}`) : ""].filter(Boolean).join("\n");
277
+ const box = new Box(outputPad, 0);
278
+ box.addChild(new Text(body, 0, 0));
279
+ return box;
280
+ });
94
281
  const tools = new Map<string, any>();
282
+ const rawTools = new Map<string, any>();
283
+ const restoreTaskAfterUnavailableEvaluation = (workflowId: number, taskId: number, type: string) => {
284
+ const taskStatus = type === "collect" ? "awaiting_user" : "running";
285
+ const workflowStatus = type === "collect" ? "awaiting_user" : "running";
286
+ db.prepare("UPDATE workflow_tasks SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND workflow_id = ? AND status = 'evaluating'").run(taskStatus, taskId, workflowId);
287
+ db.prepare("UPDATE workflows SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'evaluating'").run(workflowStatus, workflowId);
288
+ };
289
+ const automaticTaskEvaluation = async (ctx: ExtensionContext, workflowId: number, taskId: number, result: string) => {
290
+ const current = task(workflowId);
291
+ if (!current?.criteria || current.id !== taskId)
292
+ return undefined;
293
+ const item = row(workflowId);
294
+ const decision = await evaluateWorkflowTask(ctx, { workflow: { title: item?.title ?? "", source: item?.source ?? "" }, task: { type: current.type, instruction: current.instruction, criteria: current.criteria }, result });
295
+ if (!decision)
296
+ return undefined;
297
+ const message = decision.outcome === "accept" ? `Task #${current.position} accepted by the configured evaluator.` : decision.outcome === "retry" ? `Task #${current.position} will be retried.` : decision.outcome === "fail" ? `Task #${current.position} ended with a terminal failure; workflow failed.` : `Task #${current.position} evaluation is unresolved; work remains blocked.`;
298
+ audit(`task_evaluation_${decision.outcome}`, { taskId, taskPosition: current.position, ...decision, message }, undefined, workflowId, true);
299
+ if (!["accept", "retry", "fail"].includes(decision.outcome)) {
300
+ restoreTaskAfterUnavailableEvaluation(workflowId, taskId, current.type);
301
+ blockEvaluation(decision.reasoning, decision as any, undefined, workflowId);
302
+ return decision;
303
+ }
304
+ const evaluate = rawTools.get("sequential_workflow_evaluate");
305
+ if (!evaluate)
306
+ throw new Error("Sequential Workflow evaluator is unavailable.");
307
+ const applied = await evaluate.execute("system-one-evaluation", { workflowId, taskId, outcome: decision.outcome, reasoning: decision.reasoning, decision: { evaluator: decision.evaluator, model: decision.model, confidence: decision.confidence, probabilities: decision.probabilities } }, ctx.signal, undefined, ctx);
308
+ if (applied.details?.next?.completed)
309
+ audit("workflow_completed", { evaluator: decision.evaluator, model: decision.model, message: `Workflow #${workflowId} completed.` }, undefined, workflowId, true);
310
+ return { ...decision, applied };
311
+ };
95
312
  const api = new Proxy(host, { get(target, key) {
96
313
  if (key !== "registerTool")
97
314
  return Reflect.get(target, key);
98
315
  return (definition: any) => {
316
+ rawTools.set(definition.name, definition);
99
317
  const wrapped = { ...definition, async execute(id: string, params: any, signal: any, update: any, ctx: ExtensionContext) {
100
318
  return transaction(ctx, async () => {
101
319
  assertOperation(definition.name, params);
320
+ const pendingBefore = pendingWorkflow();
321
+ const taskBefore = params.workflowId !== undefined ? task(params.workflowId) : undefined;
102
322
  const result = await definition.execute(id, params, signal, update, ctx);
103
- if (definition.name === "sequential_workflow_create" || definition.name.endsWith("create_from_template")) {
104
- save([...stack(), result.details.workflowId]);
323
+ if (definition.name === "sequential_workflow_create" || definition.name === "sequential_workflow_create_subworkflow" || definition.name.endsWith("create_from_template")) {
324
+ const workflowId = result.details.workflowId as number;
325
+ if (pendingBefore === workflowId)
326
+ restoreDefinitionTools(workflowId);
327
+ save([...new Set([...stack(), workflowId])]);
328
+ audit("workflow_hydrated", { message: `Workflow #${workflowId} created and ready for its current task.` }, undefined, workflowId, true);
329
+ }
330
+ if (definition.name === "sequential_workflow_record_result" && result.details?.needsEvaluation) {
331
+ const automatic = await automaticTaskEvaluation(ctx, params.workflowId, params.taskId, params.result);
332
+ if (automatic?.applied)
333
+ return automatic.applied;
334
+ }
335
+ if (definition.name === "sequential_workflow_evaluate" && result.details?.failed) {
336
+ const phase = result.details.terminal ? "task_terminal_failed" : "failed_attempt_limit";
337
+ audit(phase, { taskId: params.taskId, taskPosition: taskBefore?.position, message: result.details.terminal ? `Task #${taskBefore?.position ?? params.taskId} ended with a terminal failure; workflow failed.` : `Task #${taskBefore?.position ?? params.taskId} reached its retry limit; workflow failed.` }, undefined, params.workflowId, true);
105
338
  }
339
+ else if (definition.name === "sequential_workflow_evaluate" && (params.outcome === "retry" || params.accepted === false))
340
+ audit("task_retry", { taskId: params.taskId, taskPosition: taskBefore?.position, message: `Task #${taskBefore?.position ?? params.taskId} will be retried.` }, undefined, params.workflowId, true);
341
+ if ((definition.name === "sequential_workflow_evaluate" || definition.name === "sequential_workflow_record_result") && result.details?.next?.completed)
342
+ audit("workflow_completed", { message: `Workflow #${params.workflowId} completed.` }, undefined, params.workflowId, true);
106
343
  return result;
107
344
  });
108
345
  } };
@@ -110,46 +347,98 @@ export function workflowHarness(host: ExtensionAPI, db: DatabaseSync) {
110
347
  host.registerTool(wrapped);
111
348
  };
112
349
  } });
113
- let namedActivation = false;
114
350
  host.on("before_agent_start", (_event, ctx) => context.run(ctx, () => {
115
- if (!namedActivation)
351
+ // Requirement checks are emitted immediately by the input/tool handler so
352
+ // their visible custom message is persisted before any model turn begins.
353
+ return;
354
+ }));
355
+ host.on("before_agent_start", (_event, ctx) => context.run(ctx, () => {
356
+ const pending = pendingWorkflow();
357
+ const mode = definitionMode(pending);
358
+ if (!pending || !mode || mode.origin !== "user_input" || definitionEvaluationSent(pending))
116
359
  return;
117
- namedActivation = false;
118
- const id = focus();
119
- const t = id ? task(id) : undefined;
120
- if (t?.type === "collect")
121
- db.prepare("UPDATE workflow_collect_checkpoint SET user_entry_id=? WHERE task_id=?").run(userEntry(), t.id);
360
+ addEvent(pending, "definition_mode_evaluated", { requiresSequentialWorkflow: true });
361
+ return { message: { customType: "sequential-workflow-evaluation", content: "Sequential Workflow is required for this request.", display: true, details: { workflowId: pending, requiresSequentialWorkflow: true } } };
122
362
  }));
123
- // Named activation is resolved before the model can invent a replacement plan.
124
- host.on("input", async (event, ctx) => {
125
- namedFailure = false;
126
- const match = event.text.trim().match(/^(?:ative|execute|inicie|activate|run|start)\s+(?:(?:o|the)\s+)?sequential[ -]workflow\s+([\w.-]+)\s*$/i);
127
- if (!match)
363
+ host.on("before_agent_start", (_event, ctx) => context.run(ctx, () => {
364
+ const pending = pendingWorkflow();
365
+ if (!pending)
128
366
  return;
129
- try {
130
- const result = await tools.get("sequential_workflow_create_from_template").execute("named-start", { path: match[1] }, undefined, undefined, ctx);
131
- namedActivation = true;
132
- return { action: "transform" as const, text: `${event.text}\n${result.content[0].text}\nThe named template was executed by the harness. Do not create another workflow; follow its current task.` };
367
+ const message = announceDefinitionMode(pending);
368
+ return message ? { message } : undefined;
369
+ }));
370
+ const workflowKeyword = /\b(?:sequential[\s-]workflow|workflow[\s-]sequencial|fluxo de trabalho sequencial)\b/i;
371
+ // A keyword starts semantic evaluation; the configured evaluator, rather than
372
+ // local verb matching, decides whether the user is requesting an execution.
373
+ const workflowKeywordDetected = (text: string) => workflowKeyword.test(text);
374
+ const skillRequiresWorkflowEvaluation = (content: string) => workflowKeywordDetected(content);
375
+ // Evaluation starts when a user message or a read Skill contains the keyword.
376
+ // The evaluator alone decides whether that evidence is an explicit request.
377
+ host.on("input", async (event, ctx) => context.run(ctx, async () => {
378
+ if (event.source === "extension")
379
+ return;
380
+ // An unavailable requirement evaluator must not permanently lock a session
381
+ // with no active workflow. A later user request is new evidence and can be
382
+ // evaluated normally if it explicitly requests a workflow.
383
+ if (event.source !== "extension" && focus() === undefined && requirementBlock()) {
384
+ clearRequirementBlock();
385
+ audit("requirement_block_released", { message: "A new user input released the unresolved Sequential Workflow evaluation block." }, "user_input");
133
386
  }
134
- catch (error) {
135
- namedFailure = true;
136
- ctx.ui.notify(String(error), "error");
137
- return { action: "transform" as const, text: `${event.text}\nNamed workflow activation failed: ${String(error)}. Report the failure; do not invent a substitute workflow.` };
387
+ const focused = focus();
388
+ const focusedTask = focused ? task(focused) : undefined;
389
+ const evaluationConfig = await workflowEvaluationConfig().catch(() => undefined);
390
+ if (evaluationConfig?.modelType === "system_one" && focused && focusedTask?.type === "collect" && focusedTask.status === "awaiting_user") {
391
+ const record = rawTools.get("sequential_workflow_record_result");
392
+ if (!record)
393
+ throw new Error("Sequential Workflow result recorder is unavailable.");
394
+ const recorded = await transaction(ctx, () => record.execute("system-one-collect", { workflowId: focused, taskId: focusedTask.id, phase: "collect", result: event.text }, ctx.signal, undefined, ctx));
395
+ if (recorded.details?.needsEvaluation)
396
+ await transaction(ctx, () => automaticTaskEvaluation(ctx, focused, focusedTask.id, event.text));
397
+ return { action: "continue" as const };
138
398
  }
139
- });
399
+ if (pendingWorkflow() && explicitUserOverride(event.text))
400
+ await transaction(ctx, () => { cancelPendingWorkflowByUser(event.text); });
401
+ if (focus() !== undefined || !event.text.trim() || !workflowKeywordDetected(event.text))
402
+ return;
403
+ clearRequirementBlock();
404
+ audit("requirement_check_started", { message: "Checking whether Sequential Workflow is required…" }, "user_input", undefined, true);
405
+ const decision = await classifyRequirement(ctx, "user_input", event.text, event.text);
406
+ const message = decision.status === "required" ? "Sequential Workflow is required for this request." : decision.status === "not_required" ? "Sequential Workflow is not required for this request." : decision.status === "uncertain" ? "Sequential Workflow decision is uncertain; work remains blocked." : "Sequential Workflow requirement could not be classified.";
407
+ audit(`requirement_classified_${decision.status}`, { ...decision, requiresSequentialWorkflow: decision.status === "required", message }, "user_input", undefined, true);
408
+ if (decision.status !== "required") {
409
+ if (decision.status !== "not_required") await transaction(ctx, () => { blockEvaluation(decision.reasoning, decision as any, "user_input"); });
410
+ return;
411
+ }
412
+ await transaction(ctx, async () => {
413
+ if (focus() === undefined) {
414
+ const workflowId = createPendingWorkflow("user_input", event.text, decision.reasoning);
415
+ const template = decision.templateId ? (await templateCandidates()).find(candidate => candidate.id === decision.templateId) : undefined;
416
+ startDefinitionMode(workflowId, "user_input", event.text, decision.activation ?? "definition", template?.source);
417
+ audit("definition_mode_started", { evaluator: decision.evaluator, model: decision.model, message: `Sequential Workflow definition mode started for workflow #${workflowId}.` }, "user_input", workflowId);
418
+ }
419
+ });
420
+ }));
140
421
  let continuationKey = "";
141
422
  let continuations = 0;
142
423
  host.on("input", (event) => { if (event.source !== "extension")
143
424
  continuations = 0; });
425
+ host.on("agent_before_settle", (event, ctx) => context.run(ctx, () => {
426
+ const pending = pendingWorkflow();
427
+ if (pending && definitionMode(pending) && event.context.canContinue)
428
+ return { continue: true };
429
+ }));
144
430
  host.on("agent_end", (event, ctx) => context.run(ctx, () => {
145
431
  const last = event.messages.filter(m => m.role === "assistant").at(-1);
146
432
  if (last?.role === "assistant" && ["aborted", "error"].includes(last.stopReason))
147
433
  return;
148
434
  const id = focus();
435
+ const pending = pendingWorkflow();
436
+ if (pending && definitionMode(pending))
437
+ return;
149
438
  const t = id ? task(id) : undefined;
150
- if (!t || t.status === "awaiting_user" || t.status === "waiting_subworkflow")
439
+ if (requirementBlock() || !pending && (!t || t.status === "awaiting_user" || t.status === "waiting_subworkflow" || t.status === "evaluating"))
151
440
  return;
152
- const key = `${session()}:${id}:${t.id}:${t.status}:${t.attempts}`;
441
+ const key = pending ? `${session()}:${pending}:pending_definition` : `${session()}:${id}:${t.id}:${t.status}:${t.attempts}`;
153
442
  if (key !== continuationKey) {
154
443
  continuationKey = key;
155
444
  continuations = 0;
@@ -158,14 +447,56 @@ export function workflowHarness(host: ExtensionAPI, db: DatabaseSync) {
158
447
  ctx.ui.notify("Workflow remains pending: automatic continuation limit reached. Inspect the current task before resuming.", "warning");
159
448
  return;
160
449
  }
161
- host.sendMessage({ customType: "sequential-workflow-state", display: false, content: `Workflow ${id}, task ${t.id} is still ${t.status}. Complete the current phase and record its transition; do not claim workflow completion.` }, { triggerTurn: true, deliverAs: "followUp" });
450
+ const content = pending
451
+ ? `Workflow ${pending} requires a definition. Do not perform work or use external tools. Create the focused pending workflow using the existing workflow creation tools.`
452
+ : `Workflow ${id}, task ${t.id} is still ${t.status}. Complete the current phase and record its transition; do not claim workflow completion.`;
453
+ host.sendMessage({ customType: "sequential-workflow-state", display: false, content }, { triggerTurn: true, deliverAs: "followUp" });
162
454
  }));
163
455
  // Pi preflights sibling calls before executing them. Allow only one call per
164
456
  // model turn while focused (including a create call that establishes focus).
165
457
  let dispatched = false;
458
+ const latestUserRequest = (ctx: ExtensionContext) => {
459
+ const content = ctx.sessionManager.getBranch().filter((entry: any) => entry.type === "message" && entry.message?.role === "user").at(-1)?.message?.content;
460
+ return typeof content === "string" ? content : Array.isArray(content) ? content.map((part: any) => part.text ?? "").join("") : "";
461
+ };
166
462
  host.on("turn_start", () => { dispatched = false; });
167
- host.on("tool_call", (event, ctx) => context.run(ctx, () => {
463
+ host.on("tool_call", async (event, ctx) => context.run(ctx, async () => {
464
+ if (event.toolName === "read" && typeof event.input?.path === "string" && basename(event.input.path) === "SKILL.md" && focus() === undefined) {
465
+ try {
466
+ const path = resolve(ctx.cwd, event.input.path);
467
+ const content = await readFile(path, "utf8");
468
+ {
469
+ if (skillRequiresWorkflowEvaluation(content)) {
470
+ const userRequest = latestUserRequest(ctx);
471
+ audit("requirement_check_started", { path, message: "Checking whether Sequential Workflow is required…" }, "skill", undefined, true);
472
+ const decision = await classifyRequirement(ctx, "skill", content, userRequest);
473
+ const message = decision.status === "required" ? "Sequential Workflow is required for this request." : decision.status === "not_required" ? "Sequential Workflow is not required for this request." : decision.status === "uncertain" ? "Sequential Workflow decision is uncertain; work remains blocked." : "Sequential Workflow requirement could not be classified.";
474
+ audit(`requirement_classified_${decision.status}`, { path, ...decision, requiresSequentialWorkflow: decision.status === "required", message }, "skill", undefined, true);
475
+ if (decision.status === "required") {
476
+ await transaction(ctx, async () => {
477
+ if (focus() === undefined) {
478
+ const workflowId = createPendingWorkflow("skill", path, decision.reasoning);
479
+ startDefinitionMode(workflowId, "skill", userRequest, decision.activation ?? "definition", undefined, path);
480
+ audit("definition_mode_started", { path, message: `Sequential Workflow definition mode started for workflow #${workflowId}.` }, "skill", workflowId, true);
481
+ }
482
+ });
483
+ // Preserve the normal read result. The next model decision receives
484
+ // the Skill it requested plus the definition-mode system instruction.
485
+ return;
486
+ }
487
+ else if (decision.status !== "not_required")
488
+ await transaction(ctx, () => { blockEvaluation(decision.reasoning, decision as any, "skill"); });
489
+ }
490
+ }
491
+ }
492
+ catch {
493
+ // The original read tool reports path and access errors.
494
+ }
495
+ }
168
496
  const id = focus();
497
+ const blockedRequirement = requirementBlock();
498
+ if (blockedRequirement && !["sequential_workflow_cancel", "sequential_workflow_status"].includes(event.toolName))
499
+ return { block: true, terminate: true, reason: `Sequential Workflow evaluation is unresolved: ${blockedRequirement.reason}` };
169
500
  if (id === undefined && !event.toolName.startsWith("sequential_workflow_create")) {
170
501
  dispatched = true;
171
502
  return;
@@ -174,6 +505,12 @@ export function workflowHarness(host: ExtensionAPI, db: DatabaseSync) {
174
505
  return { block: true, reason: "Sequential Workflow requires one tool call per model turn." };
175
506
  const t = id ? task(id) : undefined;
176
507
  const workflowTool = event.toolName.startsWith("sequential_workflow_");
508
+ const pending = pendingWorkflow();
509
+ const mode = definitionMode(pending);
510
+ if (pending && mode && !definitionTools(mode.activation).includes(event.toolName))
511
+ return { block: true, reason: `Definition mode permits only: ${definitionTools(mode.activation).join(", ")}.` };
512
+ if (pending !== undefined && !workflowTool && !mode)
513
+ return { block: true, reason: "Sequential Workflow creation is required before external tools may run." };
177
514
  if (t && !workflowTool && (t.type !== "action" || t.status !== "running"))
178
515
  return { block: true, reason: `Task ${t.id} is ${t.type}/${t.status}; external tools are blocked until the required workflow transition.` };
179
516
  try {
@@ -185,11 +522,40 @@ export function workflowHarness(host: ExtensionAPI, db: DatabaseSync) {
185
522
  }
186
523
  dispatched = true;
187
524
  }));
525
+ host.on("context_with_system", (event, ctx) => context.run(ctx, () => {
526
+ const workflowId = pendingWorkflow();
527
+ const mode = definitionMode(workflowId);
528
+ if (!workflowId || !mode)
529
+ return;
530
+ const messages = event.messages.filter((message: any) => !(message.role === "custom" && message.customType === "sequential-workflow-state"));
531
+ const creationTool = definitionCreationTool(mode.activation);
532
+ const creation = mode.origin === "skill" && mode.activation === "template"
533
+ ? "The Skill just read specifies the template. Use that template now."
534
+ : mode.activation === "template"
535
+ ? mode.templatePath ? `Use the explicitly requested template path ${mode.templatePath}.` : `Use the exact JSON template name explicitly required by the supplied Skill. A relative template name is relative to the directory containing ${mode.skillPath ?? "the Skill"}.`
536
+ : "Define the ordered workflow from the original request and the supplied Skill.";
537
+ const permitted = definitionTools(mode.activation).join(", ");
538
+ const instruction = { role: "system", content: `Sequential Workflow definition mode is active for workflow #${workflowId}. You MUST create or hydrate this workflow before performing the user's requested work. ${creation} The only permitted tools are: ${permitted}. Do not perform the requested operation, claim completion, use any other tool, or bypass workflow creation. When ready, make exactly one call to ${creationTool}; the harness will reject the other workflow creation route.` } as any;
539
+ // A Skill cutover must steer the immediate post-read continuation without
540
+ // replacing the Skill's normal tool result. Keep the steering message first.
541
+ if (mode.origin === "skill")
542
+ messages.unshift(instruction);
543
+ else
544
+ messages.push(instruction);
545
+ return { messages };
546
+ }));
188
547
  host.on("context", (event, ctx) => context.run(ctx, () => {
189
548
  const messages = event.messages.filter(m => !(m.role === "custom" && m.customType === "sequential-workflow-state"));
190
549
  const id = focus();
191
- if (id !== undefined)
192
- messages.push({ role: "custom", customType: "sequential-workflow-state", display: false, timestamp: Date.now(), content: `Session workflow focus: ${id}. Current task: ${JSON.stringify(task(id))}. Work only on this task. Pass workflowId and taskId for transitions. Collect requires a new user response. External tools are blocked outside a running Action. Use one tool per turn. User commands /workflow-suspend and /workflow-focus control focus.` });
550
+ if (id !== undefined) {
551
+ const pending = pendingWorkflow();
552
+ const mode = definitionMode(pending);
553
+ const creationTool = mode ? definitionCreationTool(mode.activation) : "sequential_workflow_create";
554
+ const templateHint = mode?.activation === "template" ? " Use the exact JSON template explicitly required by the supplied Skill." : "";
555
+ messages.push({ role: "custom", customType: "sequential-workflow-state", display: false, timestamp: Date.now(), content: pending
556
+ ? `Session workflow focus: ${pending}. This workflow is pending definition because Sequential Workflow creation is required. Do not perform work or use external tools. Create or initialize this focused workflow using ${creationTool}.${templateHint}`
557
+ : `Session workflow focus: ${id}. Current task: ${JSON.stringify(task(id))}. Work only on this task. Pass workflowId and taskId for transitions. Collect requires a new user response. External tools are blocked outside a running Action. Use one tool per turn. User commands /workflow-suspend and /workflow-focus control focus.` });
558
+ }
193
559
  return { messages };
194
560
  }));
195
561
  const command = (name: string, description: string, work: (args: string) => void) => host.registerCommand(name, { description, handler: async (args, ctx) => {
@@ -234,5 +600,5 @@ export function workflowHarness(host: ExtensionAPI, db: DatabaseSync) {
234
600
  }
235
601
  save(chain);
236
602
  });
237
- return { api, session, own, transaction, context, checkpoint };
603
+ return { api, session, own, transaction, context, checkpoint, pendingWorkflow };
238
604
  }
@@ -1,7 +1,7 @@
1
1
  import { notFound } from "next/navigation";
2
2
  import { ConsolePage } from "@/components/console-page";
3
3
  import { sectionMetadata } from "@/lib/page-metadata";
4
- const sections = new Set(["chat", "profiles", "settings", "env", "soul", "context-memory", "guardrails", "skills", "tools", "packages", "pulses", "extensions", "applications", "api-server", "pi-console-webui", "skill-sources"]);
4
+ const sections = new Set(["chat", "profiles", "settings", "model", "env", "soul", "context-memory", "guardrails", "skills", "tools", "packages", "pulses", "sequential-workflows", "extensions", "applications", "models", "api-server", "pi-console-webui", "skill-sources"]);
5
5
  export async function generateMetadata({ params, searchParams }: { params: Promise<{ section: string }>; searchParams: Promise<{ profile?: string }> }) {
6
6
  return sectionMetadata((await params).section, (await searchParams).profile);
7
7
  }
@@ -0,0 +1,22 @@
1
+ import { NextResponse } from "next/server";
2
+ import { authenticated } from "@/lib/auth";
3
+ import { readModelsConfig, writeModelsConfig } from "@/lib/admin-models";
4
+
5
+ export async function GET() {
6
+ if (!(await authenticated())) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
7
+ try {
8
+ return NextResponse.json({ content: await readModelsConfig() });
9
+ } catch (error) {
10
+ return NextResponse.json({ error: error instanceof Error ? error.message : "Unable to read models.json" }, { status: 500 });
11
+ }
12
+ }
13
+
14
+ export async function PUT(request: Request) {
15
+ if (!(await authenticated())) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
16
+ try {
17
+ const { content } = await request.json();
18
+ return NextResponse.json({ content: await writeModelsConfig(content) });
19
+ } catch (error) {
20
+ return NextResponse.json({ error: error instanceof Error ? error.message : "Invalid models.json" }, { status: 400 });
21
+ }
22
+ }
@@ -14,6 +14,9 @@ body:has(.terminal-page) { background: #0d1117; }
14
14
  .terminal-host .xterm { width: 100%; max-width: 100%; height: 100%; overflow: hidden; }
15
15
  .terminal-host .xterm-viewport { background-color: #0d1117 !important; }
16
16
  button, input, textarea, select { font: inherit; }
17
+ /* Form labels are emphasized; entered and selected values remain regular weight. */
18
+ label { font-weight: 600; }
19
+ input, textarea, select, .cm-editor, .w-md-editor { font-weight: 400; }
17
20
  select, select option { background-color: #ffffff !important; }
18
21
  .bg-zinc-950, .bg-zinc-900 { background-color: #ffffff !important; }
19
22
  .bg-zinc-800 { background-color: #e4e4e7 !important; }