@agimon-ai/doompi-workflow 0.0.1-alpha.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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/dispatcher.cjs +2 -0
  4. package/dist/dispatcher.cjs.map +1 -0
  5. package/dist/dispatcher.d.cts +6 -0
  6. package/dist/dispatcher.d.cts.map +1 -0
  7. package/dist/dispatcher.d.mts +6 -0
  8. package/dist/dispatcher.d.mts.map +1 -0
  9. package/dist/dispatcher.mjs +2 -0
  10. package/dist/dispatcher.mjs.map +1 -0
  11. package/dist/extension.cjs +2 -0
  12. package/dist/extension.cjs.map +1 -0
  13. package/dist/extension.d.cts +6 -0
  14. package/dist/extension.d.cts.map +1 -0
  15. package/dist/extension.d.mts +6 -0
  16. package/dist/extension.d.mts.map +1 -0
  17. package/dist/extension.mjs +2 -0
  18. package/dist/extension.mjs.map +1 -0
  19. package/dist/extensions/dispatcher.cjs +2 -0
  20. package/dist/extensions/dispatcher.cjs.map +1 -0
  21. package/dist/extensions/dispatcher.d.cts +2 -0
  22. package/dist/extensions/dispatcher.d.mts +2 -0
  23. package/dist/extensions/dispatcher.mjs +2 -0
  24. package/dist/extensions/dispatcher.mjs.map +1 -0
  25. package/dist/extensions/pi.cjs +2 -0
  26. package/dist/extensions/pi.cjs.map +1 -0
  27. package/dist/extensions/pi.d.cts +2 -0
  28. package/dist/extensions/pi.d.mts +2 -0
  29. package/dist/extensions/pi.mjs +2 -0
  30. package/dist/extensions/pi.mjs.map +1 -0
  31. package/dist/index.cjs +1 -0
  32. package/dist/index.d.cts +3 -0
  33. package/dist/index.d.mts +3 -0
  34. package/dist/index.mjs +1 -0
  35. package/dist/leader.cjs +2 -0
  36. package/dist/leader.cjs.map +1 -0
  37. package/dist/leader.mjs +2 -0
  38. package/dist/leader.mjs.map +1 -0
  39. package/dist/types.cjs +2 -0
  40. package/dist/types.cjs.map +1 -0
  41. package/dist/types.mjs +2 -0
  42. package/dist/types.mjs.map +1 -0
  43. package/dist/workflow/backgroundWork.cjs +2 -0
  44. package/dist/workflow/backgroundWork.cjs.map +1 -0
  45. package/dist/workflow/backgroundWork.mjs +2 -0
  46. package/dist/workflow/backgroundWork.mjs.map +1 -0
  47. package/dist/workflow/doomOverlay.cjs +2 -0
  48. package/dist/workflow/doomOverlay.cjs.map +1 -0
  49. package/dist/workflow/doomOverlay.mjs +2 -0
  50. package/dist/workflow/doomOverlay.mjs.map +1 -0
  51. package/dist/workflow/overlayText.cjs +2 -0
  52. package/dist/workflow/overlayText.cjs.map +1 -0
  53. package/dist/workflow/overlayText.mjs +2 -0
  54. package/dist/workflow/overlayText.mjs.map +1 -0
  55. package/dist/workflow/piExtension.cjs +10 -0
  56. package/dist/workflow/piExtension.cjs.map +1 -0
  57. package/dist/workflow/piExtension.mjs +10 -0
  58. package/dist/workflow/piExtension.mjs.map +1 -0
  59. package/dist/workflow/piToolBridge.cjs +11 -0
  60. package/dist/workflow/piToolBridge.cjs.map +1 -0
  61. package/dist/workflow/piToolBridge.mjs +11 -0
  62. package/dist/workflow/piToolBridge.mjs.map +1 -0
  63. package/dist/workflow/piTools.cjs +4 -0
  64. package/dist/workflow/piTools.cjs.map +1 -0
  65. package/dist/workflow/piTools.mjs +4 -0
  66. package/dist/workflow/piTools.mjs.map +1 -0
  67. package/dist/workflow/rendering.cjs +2 -0
  68. package/dist/workflow/rendering.cjs.map +1 -0
  69. package/dist/workflow/rendering.mjs +2 -0
  70. package/dist/workflow/rendering.mjs.map +1 -0
  71. package/dist/workflow/workflowFooterStatus.cjs +2 -0
  72. package/dist/workflow/workflowFooterStatus.cjs.map +1 -0
  73. package/dist/workflow/workflowFooterStatus.mjs +2 -0
  74. package/dist/workflow/workflowFooterStatus.mjs.map +1 -0
  75. package/dist/workflow/workflowInspector.cjs +2 -0
  76. package/dist/workflow/workflowInspector.cjs.map +1 -0
  77. package/dist/workflow/workflowInspector.mjs +2 -0
  78. package/dist/workflow/workflowInspector.mjs.map +1 -0
  79. package/dist/workflow/workflowLauncher.cjs +2 -0
  80. package/dist/workflow/workflowLauncher.cjs.map +1 -0
  81. package/dist/workflow/workflowLauncher.mjs +2 -0
  82. package/dist/workflow/workflowLauncher.mjs.map +1 -0
  83. package/dist/workflow/workflowOverlay.cjs +2 -0
  84. package/dist/workflow/workflowOverlay.cjs.map +1 -0
  85. package/dist/workflow/workflowOverlay.mjs +2 -0
  86. package/dist/workflow/workflowOverlay.mjs.map +1 -0
  87. package/dist/workflow/workflowPicker.cjs +2 -0
  88. package/dist/workflow/workflowPicker.cjs.map +1 -0
  89. package/dist/workflow/workflowPicker.mjs +2 -0
  90. package/dist/workflow/workflowPicker.mjs.map +1 -0
  91. package/dist/workflow/workflowPrompt.cjs +2 -0
  92. package/dist/workflow/workflowPrompt.cjs.map +1 -0
  93. package/dist/workflow/workflowPrompt.mjs +2 -0
  94. package/dist/workflow/workflowPrompt.mjs.map +1 -0
  95. package/dist/workflow/workflowRunPanel.cjs +2 -0
  96. package/dist/workflow/workflowRunPanel.cjs.map +1 -0
  97. package/dist/workflow/workflowRunPanel.mjs +2 -0
  98. package/dist/workflow/workflowRunPanel.mjs.map +1 -0
  99. package/dist/workflow/workflowStatusRow.cjs +2 -0
  100. package/dist/workflow/workflowStatusRow.cjs.map +1 -0
  101. package/dist/workflow/workflowStatusRow.mjs +2 -0
  102. package/dist/workflow/workflowStatusRow.mjs.map +1 -0
  103. package/dist/workflow/workflowStepMessage.cjs +2 -0
  104. package/dist/workflow/workflowStepMessage.cjs.map +1 -0
  105. package/dist/workflow/workflowStepMessage.mjs +2 -0
  106. package/dist/workflow/workflowStepMessage.mjs.map +1 -0
  107. package/package.json +82 -0
  108. package/skills/workflow-recovery/SKILL.md +73 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piTools.mjs","names":[],"sources":["../../src/workflow/piTools.ts"],"sourcesContent":["import type {\n AgentToolResult,\n AgentToolUpdateCallback,\n ExtensionAPI,\n ExtensionContext,\n} from '@earendil-works/pi-coding-agent';\nimport {\n createEmbeddedWorkflowFeature,\n type EmbeddedWorkflowFeature,\n type WorkflowRunRecord,\n} from '@agimon-ai/workflow-mcp';\nimport { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\nimport {\n launchedRunSummary,\n launchHandoffSummary,\n PI_SESSION_ENV,\n resolveMaxConcurrent,\n toAgentToolResult,\n toolResultText,\n withOptions,\n} from './piToolBridge';\n\nconst AGIFLOW_JOB_ID_ENV = 'AGIFLOW_JOB_ID';\nconst AGIFLOW_JOB_KIND_ENV = 'AGIFLOW_JOB_KIND';\n\nconst PI_LAUNCH_FIELDS = {\n workflowPath: true,\n workspace: true,\n runner: true,\n name: true,\n env: true,\n prompt: true,\n job: true,\n inputs: true,\n} as const;\n\nexport const WORKFLOW_PI_TOOL_NAMES = ['list_workflows', 'launch_workflow', 'workflow_run'] as const;\nconst [LIST_WORKFLOWS_TOOL_NAME, LAUNCH_WORKFLOW_TOOL_NAME, WORKFLOW_RUN_TOOL_NAME] = WORKFLOW_PI_TOOL_NAMES;\n\nconst RUN_SELECTOR_FIELDS = {\n runKey: z.string().min(1).describe('Run key of the workflow run.'),\n workspace: z.string().optional().describe('Workspace the run belongs to.'),\n} as const;\n\nconst workflowRunInputSchema = z.discriminatedUnion('action', [\n z.object({ action: z.literal('status'), ...RUN_SELECTOR_FIELDS }).strict(),\n z.object({ action: z.literal('tail'), ...RUN_SELECTOR_FIELDS }).strict(),\n z.object({ action: z.literal('follow'), ...RUN_SELECTOR_FIELDS }).strict(),\n z.object({ action: z.literal('open'), ...RUN_SELECTOR_FIELDS }).strict(),\n z.object({ action: z.literal('pause'), ...RUN_SELECTOR_FIELDS, reason: z.string().optional() }).strict(),\n z.object({ action: z.literal('resume'), ...RUN_SELECTOR_FIELDS, reason: z.string().optional() }).strict(),\n z.object({ action: z.literal('stop'), ...RUN_SELECTOR_FIELDS, reason: z.string().optional() }).strict(),\n z\n .object({\n action: z.literal('recover'),\n ...RUN_SELECTOR_FIELDS,\n dryRun: z.boolean().optional(),\n job: z.string().optional(),\n runner: z.string().optional(),\n })\n .strict(),\n]);\ntype WorkflowRunInput = z.infer<typeof workflowRunInputSchema>;\ntype WorkflowRunSelector = Pick<WorkflowRunInput, 'runKey' | 'workspace'>;\n\nexport type WorkflowLaunchInput = z.infer<ReturnType<EmbeddedWorkflowFeature['runTool']['getInputSchema']>>;\n\n/**\n * Where a launch was answered from, which decides what the caller is told.\n *\n * Tagged rather than inferred from an absent field: the failure carries an\n * `unknown`, and `unknown` includes `undefined`, so only a tag can tell a\n * rejection apart from a result.\n */\ntype LaunchOutcome = { kind: 'value'; result: CallToolResult } | { kind: 'error'; error: unknown };\ntype LaunchAck =\n | { kind: 'settled'; outcome: LaunchOutcome }\n | { kind: 'record'; record: WorkflowRunRecord }\n | { kind: 'handoff' };\n\n/** How often the registry is asked whether the launch has produced a run. */\nconst LAUNCH_ACK_POLL_MS = 500;\n/**\n * How long to wait for that before answering without a run key.\n *\n * Long enough to cover a launcher that is merely slow, short enough that a\n * caller is never left holding a turn open for a launcher that will not answer.\n */\nconst LAUNCH_ACK_TIMEOUT_MS = 15_000;\n\nexport interface LaunchedRunQuery {\n sessionId: string | undefined;\n /** Epoch milliseconds the launch began, so an earlier run cannot match. */\n since: number;\n workflowPath: string;\n}\n\nexport interface WorkflowLaunchExecutorDependencies {\n activeRunCount?: () => Promise<number>;\n onLaunch?: (ctx: ExtensionContext) => Promise<void> | void;\n observeSession?: (sessionId: string | undefined) => void;\n rejectRunner?: (workflowPath: string, runner: string) => string | undefined;\n runTool: EmbeddedWorkflowFeature['runTool'];\n trackPendingRun: <T>(run: Promise<T>) => Promise<T>;\n /**\n * The run this launch registered, or undefined while none has appeared.\n *\n * Its presence is what lets a launch answer on the registry instead of on the\n * launcher process exiting. Left out, the launch waits for the process, which\n * is the behaviour every caller had before.\n */\n findLaunchedRun?: (query: LaunchedRunQuery) => Promise<WorkflowRunRecord | undefined>;\n /** Report a launch that fails after this call already answered \"started\". */\n onLateFailure?: (error: unknown, ctx: ExtensionContext) => void;\n launchAckPollMs?: number;\n launchAckTimeoutMs?: number;\n}\n\nexport interface WorkflowLaunchExecutor {\n execute(\n input: WorkflowLaunchInput,\n ctx: ExtensionContext,\n onUpdate?: AgentToolUpdateCallback<{ tool: string }>,\n ): Promise<CallToolResult>;\n}\n\nexport interface WorkflowPiToolDependencies {\n launchExecutor?: WorkflowLaunchExecutor;\n onLaunch?: (ctx: ExtensionContext) => Promise<void> | void;\n activeRunCount?: () => Promise<number>;\n followRun?: (input: WorkflowRunSelector, ctx: ExtensionContext) => Promise<string>;\n tailRun?: (input: WorkflowRunSelector, ctx: ExtensionContext) => Promise<string>;\n openRun?: (input: WorkflowRunSelector, ctx: ExtensionContext) => Promise<string>;\n rejectRunner?: (workflowPath: string, runner: string) => string | undefined;\n requireSessionRun?: (input: WorkflowRunSelector, sessionId: string | undefined) => Promise<unknown>;\n /**\n * Hand a recovery to a terminal launcher, returning what to tell the caller.\n *\n * Undefined means this recovery cannot be delegated, and the in-process\n * replay below is used instead.\n */\n delegateRecovery?: (\n input: { runKey: string; workspace?: string; runner?: string },\n ctx: ExtensionContext,\n ) => Promise<string | undefined>;\n observeSession?: (sessionId: string | undefined) => void;\n trackPendingRun?: <T>(run: Promise<T>) => Promise<T>;\n feature?: EmbeddedWorkflowFeature;\n recoverTool?: ReturnType<EmbeddedWorkflowFeature['createRecoverTool']>;\n controlTool?: ReturnType<EmbeddedWorkflowFeature['createControlTool']>;\n runTool?: EmbeddedWorkflowFeature['runTool'];\n listWorkflowsTool?: EmbeddedWorkflowFeature['listWorkflowsTool'];\n}\n\nfunction reportProgress(\n onUpdate: AgentToolUpdateCallback<{ tool: string }> | undefined,\n tool: string,\n message: string,\n): void {\n onUpdate?.({ content: [{ type: 'text', text: message }], details: { tool } });\n}\n\nfunction appendGuidance(\n result: AgentToolResult<{ tool: string }>,\n guidance: string,\n): AgentToolResult<{ tool: string }> {\n return { ...result, content: [...result.content, { type: 'text', text: guidance }] };\n}\n\nfunction statusAction(runKey: string): string {\n return `workflow_run {\"action\":\"status\",\"runKey\":${JSON.stringify(runKey)}}`;\n}\n\nfunction actionGuidance(action: WorkflowRunInput['action'], runKey: string): string[] {\n const status = statusAction(runKey);\n if (action === 'stop') return [`Next: call ${status} to confirm the run reached a terminal stage.`];\n if (action === 'recover') {\n return [\n `Report the recovery failure and do not retry it blindly: call ${status} first.`,\n 'Ask the user before launching fresh work when the recorded recovery is no longer valid.',\n ];\n }\n return [`Next: call ${status} to verify the recorded execution state.`];\n}\n\nconst launchFailureOptions = [\n 'Report the failure to the user with the error above. A launch failure is usually a bad workflow path, a missing launcher, or a workflow whose own pre-conditions refused.',\n 'list_workflows: confirm the workflow path exists and its description matches the work, if the path may be wrong.',\n 'workflow_run with action status: check whether an earlier attempt is already running before launching again.',\n];\n\n/**\n * An Error, whatever the launch rejected with.\n *\n * A rejection reaches here as `unknown` because it crossed a promise boundary,\n * and rethrowing that as-is hands Pi's tool layer something it cannot read a\n * message or a stack off. The original is kept as the cause rather than\n * flattened into a string.\n */\nfunction asError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value), { cause: value });\n}\n\n/** Resolves to undefined, so a race against it reads as \"nothing settled yet\". */\nfunction sleep(ms: number): Promise<undefined> {\n return new Promise((settle) => {\n const timer = setTimeout(() => settle(undefined), ms);\n timer.unref?.();\n });\n}\n\n/**\n * Wait for whichever answer arrives first: the launch settling, the run\n * appearing in the registry, or the budget running out.\n *\n * A launch that delegates to a terminal launcher settles when that launcher's\n * whole process chain closes, which on a loaded machine has been measured\n * minutes after the run itself was registered and working. The registry is the\n * earlier and more truthful signal: a run recorded there is a run that started.\n */\nasync function awaitLaunchAck(\n settled: Promise<LaunchOutcome>,\n dependencies: WorkflowLaunchExecutorDependencies,\n query: LaunchedRunQuery,\n): Promise<LaunchAck> {\n const findLaunchedRun = dependencies.findLaunchedRun;\n if (!findLaunchedRun) return { kind: 'settled', outcome: await settled };\n\n const pollMs = dependencies.launchAckPollMs ?? LAUNCH_ACK_POLL_MS;\n const deadline = Date.now() + (dependencies.launchAckTimeoutMs ?? LAUNCH_ACK_TIMEOUT_MS);\n for (;;) {\n const outcome = await Promise.race([settled, sleep(pollMs)]);\n if (outcome) return { kind: 'settled', outcome };\n // A registry read that throws must not fail the launch: the run may be\n // perfectly healthy, and the settled promise is still the fallback.\n const record = await findLaunchedRun(query).catch(() => undefined);\n if (record) return { kind: 'record', record };\n if (Date.now() >= deadline) return { kind: 'handoff' };\n }\n}\n\n/**\n * Carry a failure that lands after the caller was told the run started.\n *\n * Answering early trades the launcher's exit code for timeliness. Dropping that\n * code entirely is not part of the trade: a launch that fails a minute later\n * would otherwise leave a user waiting on a run that never existed.\n */\nfunction reportLateFailure(\n settled: Promise<LaunchOutcome>,\n dependencies: WorkflowLaunchExecutorDependencies,\n ctx: ExtensionContext,\n): void {\n void settled.then((outcome) => {\n if (outcome.kind === 'error') {\n dependencies.onLateFailure?.(asError(outcome.error), ctx);\n return;\n }\n if (outcome.result.isError) {\n dependencies.onLateFailure?.(new Error(toolResultText(outcome.result) || 'Workflow launch failed.'), ctx);\n }\n });\n}\n\nexport function createWorkflowLaunchExecutor(dependencies: WorkflowLaunchExecutorDependencies): WorkflowLaunchExecutor {\n return {\n async execute(input, ctx, onUpdate) {\n const sessionId = resolveRootSessionId(ctx.sessionManager.getSessionId());\n dependencies.observeSession?.(sessionId);\n const agiflowJobKind = input.env?.[AGIFLOW_JOB_KIND_ENV]?.trim();\n const agiflowJobId = input.env?.[AGIFLOW_JOB_ID_ENV]?.trim();\n if (Boolean(agiflowJobKind) !== Boolean(agiflowJobId)) {\n throw new Error('Agiflow workflow launches require AGIFLOW_JOB_KIND and AGIFLOW_JOB_ID together.');\n }\n if (agiflowJobKind && !input.prompt?.trim()) {\n throw new Error(\n 'Agiflow workflow launches require a non-empty prompt so user_prompt runs do not wait for terminal input.',\n );\n }\n reportProgress(onUpdate, LAUNCH_WORKFLOW_TOOL_NAME, 'Checking workflow capacity...');\n const maxConcurrent = resolveMaxConcurrent();\n const active = (await dependencies.activeRunCount?.()) ?? 0;\n if (active >= maxConcurrent) {\n throw new Error(\n withOptions(`This session is at capacity: ${active}/${maxConcurrent} workflows running.`, [\n 'Wait for a running workflow to finish, then launch again. The extension reports each one as it ends.',\n 'workflow_run with action status: show what is running, so the user can judge whether something is stuck.',\n 'workflow_run with action stop: free a slot by stopping a run the user no longer wants.',\n ]),\n );\n }\n\n if (input.runner) {\n const rejection = dependencies.rejectRunner?.(input.workflowPath, input.runner);\n if (rejection) throw new Error(rejection);\n }\n reportProgress(onUpdate, LAUNCH_WORKFLOW_TOOL_NAME, `Launching workflow ${input.workflowPath}...`);\n const workflowEnv = { ...input.env };\n if (process.env.AGIFLOW_DISPATCH_CONTEXT_FILE) {\n for (const key of [\n 'AGIFLOW_ORGANIZATION_ID',\n 'AGIFLOW_PROJECT_ID',\n 'AGIFLOW_DEVICE_ID',\n 'BACKEND_AGIFLOW_API_ENDPOINT',\n 'AGIFLOW_DISPATCH_SECRET_FILE',\n ] as const) {\n const hostValue = process.env[key];\n if (hostValue) workflowEnv[key] = hostValue;\n }\n }\n const since = Date.now();\n const launch = dependencies.trackPendingRun(\n dependencies.runTool.execute({\n ...input,\n env: { ...workflowEnv, [PI_SESSION_ENV]: sessionId },\n }),\n );\n // Folded into a value before anything races it. A promise this function\n // may stop awaiting must never be able to reject unobserved, which in\n // Node ends the whole process, Pi's TUI included.\n const settled: Promise<LaunchOutcome> = launch.then(\n (value): LaunchOutcome => ({ kind: 'value', result: value }),\n (error: unknown): LaunchOutcome => ({ kind: 'error', error }),\n );\n\n const ack = await awaitLaunchAck(settled, dependencies, {\n sessionId,\n since,\n workflowPath: input.workflowPath,\n });\n\n if (ack.kind === 'settled') {\n const outcome = ack.outcome;\n if (outcome.kind === 'error') throw asError(outcome.error);\n const result = outcome.result;\n if (result.isError)\n throw new Error(withOptions(toolResultText(result) || 'Workflow launch failed.', launchFailureOptions));\n await dependencies.onLaunch?.(ctx);\n return result;\n }\n\n reportLateFailure(settled, dependencies, ctx);\n await dependencies.onLaunch?.(ctx);\n return {\n content: [\n {\n type: 'text',\n text: ack.kind === 'record' ? launchedRunSummary(ack.record) : launchHandoffSummary(input.workflowPath),\n },\n ],\n };\n },\n };\n}\n\nexport function registerWorkflowPiTools(pi: ExtensionAPI, dependencies: WorkflowPiToolDependencies = {}): void {\n const feature = dependencies.feature ?? createEmbeddedWorkflowFeature();\n const runTool = dependencies.runTool ?? feature.runTool;\n const listWorkflowsTool = dependencies.listWorkflowsTool ?? feature.listWorkflowsTool;\n const recoverTool = dependencies.recoverTool ?? feature.createRecoverTool();\n const controlTool = dependencies.controlTool ?? feature.createControlTool();\n const trackPendingRun = dependencies.trackPendingRun ?? (<T>(run: Promise<T>): Promise<T> => run);\n const launchExecutor =\n dependencies.launchExecutor ??\n createWorkflowLaunchExecutor({\n activeRunCount: dependencies.activeRunCount,\n onLaunch: dependencies.onLaunch,\n observeSession: dependencies.observeSession,\n rejectRunner: dependencies.rejectRunner,\n runTool,\n trackPendingRun,\n });\n\n pi.registerTool({\n name: LIST_WORKFLOWS_TOOL_NAME,\n label: 'List Workflows',\n description: 'List the workflow definitions available in this repository.',\n promptSnippet: 'List available workflow definitions',\n parameters: z.toJSONSchema(listWorkflowsTool.getInputSchema()),\n async execute(_toolCallId, params) {\n return toAgentToolResult(\n LIST_WORKFLOWS_TOOL_NAME,\n await listWorkflowsTool.execute(\n params as Parameters<EmbeddedWorkflowFeature['listWorkflowsTool']['execute']>[0],\n ),\n );\n },\n });\n\n pi.registerTool({\n name: LAUNCH_WORKFLOW_TOOL_NAME,\n label: 'Launch Workflow',\n description:\n 'Start a workflow run. Returns once the run has started, not once it finishes; use workflow_run with action status or wait for the completion notice.',\n promptSnippet: 'Start a workflow run and return its identifier',\n promptGuidelines: [\n 'Before launching, call list_workflows and pick the workflow whose own description matches the work. A workflow description is the source of truth; never infer one from its filename. If nothing matches, say so and stop rather than launching an approximate fit.',\n 'launch_workflow returns when the run has STARTED, not when it has finished. Never report success from its result. Poll workflow_run with action status, or wait for the completion notice the extension delivers on its own.',\n 'When dispatching Agiflow work, pass AGIFLOW_JOB_KIND and AGIFLOW_JOB_ID together through env plus a non-empty prompt. The id alone is not a key, and omitting prompt leaves user_prompt workflows waiting for terminal input.',\n 'A workflow claims its own job in its pre step. Never claim a job yourself. JOB_ALREADY_CLAIMED or HTTP 409 means another worker won the race, so report contention and do not unlock, release, or retry automatically.',\n 'If a result says WORKFLOW_NOT_OWNED or a release failed, inspect current ownership and ask the user before any release or unlock. Never force another worker’s lock.',\n 'If a running workflow is not found, call workflow_run with action status to re-check current state before retrying.',\n 'The per-session concurrency ceiling is a real limit. If launch_workflow refuses, wait for a run to finish; do not launch through the shell to get around it.',\n ],\n parameters: z.toJSONSchema(runTool.getInputSchema().pick(PI_LAUNCH_FIELDS)),\n async execute(_toolCallId, params, _signal, onUpdate, ctx) {\n const input = params as WorkflowLaunchInput;\n const result = await launchExecutor.execute(input, ctx, onUpdate);\n const adapted = toAgentToolResult(LAUNCH_WORKFLOW_TOOL_NAME, result, launchFailureOptions);\n return appendGuidance(\n adapted,\n 'Next: call workflow_run with action status and verify the recorded stage before reporting the workflow outcome.',\n );\n },\n });\n\n pi.registerTool({\n name: WORKFLOW_RUN_TOOL_NAME,\n label: 'Workflow Run',\n description:\n 'Inspect or control one workflow run. Use action status, tail, follow, open, pause, resume, stop, or recover. Every action requires the run key returned by launch_workflow.',\n promptSnippet: 'Inspect or control a workflow run',\n promptGuidelines: [\n 'Use action status as the authority for stage, outcome, execution state, and pause cursor. A run that started is not a run that succeeded.',\n 'Use action pause or resume for cooperative lifecycle control. Pause is not stop: the run keeps its stage running, capacity, services, and heartbeat while paused.',\n 'Use action stop only when the user requests stopping a run. A user-requested stop is not a failure and needs no failure diagnosis.',\n 'Use action tail to read output, action follow to pin live progress, and action open only when a human wants the recorded launcher foregrounded.',\n 'Use action recover only after loading the `workflow-recovery` skill and gathering its required evidence. Never edit issue.md or repair.json, and verify real process and registry progress before recovery.',\n 'Never unlock or force-release an Agiflow claim from this tool.',\n ],\n parameters: z.toJSONSchema(workflowRunInputSchema),\n async execute(_toolCallId, rawParams, _signal, onUpdate, ctx) {\n const input = workflowRunInputSchema.parse(rawParams);\n const sessionId = resolveRootSessionId(ctx.sessionManager.getSessionId());\n dependencies.observeSession?.(sessionId);\n const selector: WorkflowRunSelector = { runKey: input.runKey, workspace: input.workspace };\n const requireOwnedRun = async (): Promise<unknown> => {\n if (!dependencies.requireSessionRun) {\n throw new Error('Workflow run ownership is not available in this session.');\n }\n return dependencies.requireSessionRun(selector, sessionId);\n };\n\n if (input.action === 'status') {\n const record = await requireOwnedRun();\n return {\n content: [{ type: 'text', text: JSON.stringify(record, null, 2) }],\n details: { tool: WORKFLOW_RUN_TOOL_NAME },\n };\n }\n if (input.action === 'tail') {\n if (!dependencies.tailRun) throw new Error('Reading workflow output is not available in this session.');\n await requireOwnedRun();\n reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Reading output for workflow ${input.runKey}...`);\n return {\n content: [{ type: 'text', text: await dependencies.tailRun(selector, ctx) }],\n details: { tool: WORKFLOW_RUN_TOOL_NAME },\n };\n }\n if (input.action === 'follow') {\n if (!dependencies.followRun) throw new Error('Following workflow output is not available in this session.');\n await requireOwnedRun();\n reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Following workflow ${input.runKey}...`);\n return {\n content: [{ type: 'text', text: await dependencies.followRun(selector, ctx) }],\n details: { tool: WORKFLOW_RUN_TOOL_NAME },\n };\n }\n if (input.action === 'open') {\n if (!dependencies.openRun) throw new Error('Opening a workflow launcher is not available in this session.');\n await requireOwnedRun();\n reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Opening workflow ${input.runKey}...`);\n return {\n content: [{ type: 'text', text: await dependencies.openRun(selector, ctx) }],\n details: { tool: WORKFLOW_RUN_TOOL_NAME },\n };\n }\n\n await requireOwnedRun();\n if (input.action === 'recover') {\n reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Recovering workflow ${input.runKey}...`);\n // A recovery replays the workflow, and the replay skips the launch\n // step, so every job runs in THIS process. For a workflow whose steps\n // are interactive that means an agent TUI spawned onto this process's\n // own terminal. Hand it to a launcher instead, where the workflow says\n // how. A dry run prints rather than executes, so it stays here.\n if (!input.dryRun) {\n const delegated = await dependencies.delegateRecovery?.(\n { runKey: input.runKey, workspace: input.workspace, runner: input.runner },\n ctx,\n );\n if (delegated) {\n const guidance = actionGuidance(input.action, input.runKey);\n await dependencies.onLaunch?.(ctx);\n return {\n content: [{ type: 'text', text: [delegated, guidance[0]].join('\\n\\n') }],\n details: { tool: WORKFLOW_RUN_TOOL_NAME },\n };\n }\n }\n const recoverInput = {\n dryRun: input.dryRun,\n job: input.job,\n runKey: input.runKey,\n runner: input.runner,\n workspace: input.workspace,\n };\n const result = await trackPendingRun(recoverTool.execute(recoverInput));\n const guidance = actionGuidance(input.action, input.runKey);\n const adapted = toAgentToolResult(WORKFLOW_RUN_TOOL_NAME, result, guidance);\n await dependencies.onLaunch?.(ctx);\n return appendGuidance(adapted, guidance[0]!);\n }\n\n reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Requesting ${input.action} for workflow ${input.runKey}...`);\n const result = await controlTool.execute({\n action: input.action,\n reason: input.reason,\n runKey: input.runKey,\n workspace: input.workspace,\n });\n const guidance = actionGuidance(input.action, input.runKey);\n const adapted = toAgentToolResult(WORKFLOW_RUN_TOOL_NAME, result, guidance);\n return appendGuidance(adapted, guidance[0]!);\n },\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ let e=require("@earendil-works/pi-tui");function t(t,n){return(0,e.truncateToWidth)(t,Math.max(0,n),``)}function n(n,r){let i=Math.max(0,r),a=t(n,i);return`${a}${` `.repeat(Math.max(0,i-(0,e.visibleWidth)(a)))}`}function r(r,i,a=`│`,o=`│`){let s=Math.max(0,i),c=(0,e.visibleWidth)(a)+(0,e.visibleWidth)(o);return s<=c?t(`${a}${o}`,s):`${a}${n(r,s-c)}${o}`}exports.fitLine=t,exports.frameLine=r,exports.padLine=n;
2
+ //# sourceMappingURL=rendering.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rendering.cjs","names":["truncateToWidth","visibleWidth"],"sources":["../../src/workflow/rendering.ts"],"sourcesContent":["import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\n\nconst THOUSAND = 1000;\nconst MILLION = 1_000_000;\n/** Below this many tokens the \"k\" form keeps a decimal, above it rounds. */\nconst K_DECIMAL_LIMIT = 10_000;\n\n/** Compact token count, e.g. `812`, `4.2k`, `186k`, `1.3M`. */\nexport function formatTokens(value: number): string {\n if (value < THOUSAND) return String(value);\n if (value < MILLION) return `${(value / THOUSAND).toFixed(value < K_DECIMAL_LIMIT ? 1 : 0)}k`;\n return `${(value / MILLION).toFixed(1)}M`;\n}\n\nexport function fitLine(text: string, width: number): string {\n return truncateToWidth(text, Math.max(0, width), '');\n}\n\nexport function padLine(text: string, width: number): string {\n const safeWidth = Math.max(0, width);\n const fitted = fitLine(text, safeWidth);\n return `${fitted}${' '.repeat(Math.max(0, safeWidth - visibleWidth(fitted)))}`;\n}\n\nexport function alignLine(left: string, right: string, width: number): string {\n const safeWidth = Math.max(0, width);\n const rightWidth = visibleWidth(right);\n if (!right || rightWidth >= safeWidth) return fitLine(right || left, safeWidth);\n\n const maxLeftWidth = Math.max(0, safeWidth - rightWidth - 1);\n const fittedLeft = fitLine(left, maxLeftWidth);\n const gap = ' '.repeat(Math.max(1, safeWidth - visibleWidth(fittedLeft) - rightWidth));\n return fitLine(`${fittedLeft}${gap}${right}`, safeWidth);\n}\n\nexport function frameLine(content: string, width: number, left = '│', right = '│'): string {\n const safeWidth = Math.max(0, width);\n const frameWidth = visibleWidth(left) + visibleWidth(right);\n if (safeWidth <= frameWidth) return fitLine(`${left}${right}`, safeWidth);\n return `${left}${padLine(content, safeWidth - frameWidth)}${right}`;\n}\n\nexport function packSegments(segments: string[], width: number, separator = ' '): string[] {\n const safeWidth = Math.max(0, width);\n if (safeWidth === 0) return [];\n\n const lines: string[] = [];\n let current = '';\n for (const segment of segments) {\n const candidate = current ? `${current}${separator}${segment}` : segment;\n if (visibleWidth(candidate) <= safeWidth) {\n current = candidate;\n continue;\n }\n if (current) lines.push(fitLine(current, safeWidth));\n current = fitLine(segment, safeWidth);\n }\n if (current) lines.push(current);\n return lines;\n}\n"],"mappings":"wCAcA,SAAgB,EAAQ,EAAc,EAAuB,CAC3D,OAAA,EAAOA,EAAAA,gBAAAA,CAAgB,EAAM,KAAK,IAAI,EAAG,CAAK,EAAG,EAAE,CACrD,CAEA,SAAgB,EAAQ,EAAc,EAAuB,CAC3D,IAAM,EAAY,KAAK,IAAI,EAAG,CAAK,EAC7B,EAAS,EAAQ,EAAM,CAAS,EACtC,MAAO,GAAG,IAAS,IAAI,OAAO,KAAK,IAAI,EAAG,GAAA,EAAYC,EAAAA,aAAAA,CAAa,CAAM,CAAC,CAAC,GAC7E,CAaA,SAAgB,EAAU,EAAiB,EAAe,EAAO,IAAK,EAAQ,IAAa,CACzF,IAAM,EAAY,KAAK,IAAI,EAAG,CAAK,EAC7B,GAAA,EAAaA,EAAAA,aAAAA,CAAa,CAAI,GAAA,EAAIA,EAAAA,aAAAA,CAAa,CAAK,EAE1D,OADI,GAAa,EAAmB,EAAQ,GAAG,IAAO,IAAS,CAAS,EACjE,GAAG,IAAO,EAAQ,EAAS,EAAY,CAAU,IAAI,GAC9D"}
@@ -0,0 +1,2 @@
1
+ import{truncateToWidth as e,visibleWidth as t}from"@earendil-works/pi-tui";function n(t,n){return e(t,Math.max(0,n),``)}function r(e,r){let i=Math.max(0,r),a=n(e,i);return`${a}${` `.repeat(Math.max(0,i-t(a)))}`}function i(e,i,a=`│`,o=`│`){let s=Math.max(0,i),c=t(a)+t(o);return s<=c?n(`${a}${o}`,s):`${a}${r(e,s-c)}${o}`}export{n as fitLine,i as frameLine,r as padLine};
2
+ //# sourceMappingURL=rendering.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rendering.mjs","names":[],"sources":["../../src/workflow/rendering.ts"],"sourcesContent":["import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\n\nconst THOUSAND = 1000;\nconst MILLION = 1_000_000;\n/** Below this many tokens the \"k\" form keeps a decimal, above it rounds. */\nconst K_DECIMAL_LIMIT = 10_000;\n\n/** Compact token count, e.g. `812`, `4.2k`, `186k`, `1.3M`. */\nexport function formatTokens(value: number): string {\n if (value < THOUSAND) return String(value);\n if (value < MILLION) return `${(value / THOUSAND).toFixed(value < K_DECIMAL_LIMIT ? 1 : 0)}k`;\n return `${(value / MILLION).toFixed(1)}M`;\n}\n\nexport function fitLine(text: string, width: number): string {\n return truncateToWidth(text, Math.max(0, width), '');\n}\n\nexport function padLine(text: string, width: number): string {\n const safeWidth = Math.max(0, width);\n const fitted = fitLine(text, safeWidth);\n return `${fitted}${' '.repeat(Math.max(0, safeWidth - visibleWidth(fitted)))}`;\n}\n\nexport function alignLine(left: string, right: string, width: number): string {\n const safeWidth = Math.max(0, width);\n const rightWidth = visibleWidth(right);\n if (!right || rightWidth >= safeWidth) return fitLine(right || left, safeWidth);\n\n const maxLeftWidth = Math.max(0, safeWidth - rightWidth - 1);\n const fittedLeft = fitLine(left, maxLeftWidth);\n const gap = ' '.repeat(Math.max(1, safeWidth - visibleWidth(fittedLeft) - rightWidth));\n return fitLine(`${fittedLeft}${gap}${right}`, safeWidth);\n}\n\nexport function frameLine(content: string, width: number, left = '│', right = '│'): string {\n const safeWidth = Math.max(0, width);\n const frameWidth = visibleWidth(left) + visibleWidth(right);\n if (safeWidth <= frameWidth) return fitLine(`${left}${right}`, safeWidth);\n return `${left}${padLine(content, safeWidth - frameWidth)}${right}`;\n}\n\nexport function packSegments(segments: string[], width: number, separator = ' '): string[] {\n const safeWidth = Math.max(0, width);\n if (safeWidth === 0) return [];\n\n const lines: string[] = [];\n let current = '';\n for (const segment of segments) {\n const candidate = current ? `${current}${separator}${segment}` : segment;\n if (visibleWidth(candidate) <= safeWidth) {\n current = candidate;\n continue;\n }\n if (current) lines.push(fitLine(current, safeWidth));\n current = fitLine(segment, safeWidth);\n }\n if (current) lines.push(current);\n return lines;\n}\n"],"mappings":"2EAcA,SAAgB,EAAQ,EAAc,EAAuB,CAC3D,OAAO,EAAgB,EAAM,KAAK,IAAI,EAAG,CAAK,EAAG,EAAE,CACrD,CAEA,SAAgB,EAAQ,EAAc,EAAuB,CAC3D,IAAM,EAAY,KAAK,IAAI,EAAG,CAAK,EAC7B,EAAS,EAAQ,EAAM,CAAS,EACtC,MAAO,GAAG,IAAS,IAAI,OAAO,KAAK,IAAI,EAAG,EAAY,EAAa,CAAM,CAAC,CAAC,GAC7E,CAaA,SAAgB,EAAU,EAAiB,EAAe,EAAO,IAAK,EAAQ,IAAa,CACzF,IAAM,EAAY,KAAK,IAAI,EAAG,CAAK,EAC7B,EAAa,EAAa,CAAI,EAAI,EAAa,CAAK,EAE1D,OADI,GAAa,EAAmB,EAAQ,GAAG,IAAO,IAAS,CAAS,EACjE,GAAG,IAAO,EAAQ,EAAS,EAAY,CAAU,IAAI,GAC9D"}
@@ -0,0 +1,2 @@
1
+ const e=[`◐`,`●`,`◑`],t=[`accent`,`mdCode`,`toolDiffAdded`];function n(t){let n=0;for(let e of t)n=Math.imul(n,31)+(e.codePointAt(0)??0)>>>0;return n%e.length}function r(e){if(e===void 0||!Number.isFinite(e))return t[0];let n=Math.floor(Math.max(0,Math.min(1,e))*t.length);return t[Math.min(n,t.length-1)]}function i(t,i){return t.state===`paused`?{glyph:`■`,color:`warning`}:t.state===`starting`?{glyph:`○`,color:`dim`}:{glyph:e[(i+n(t.key))%e.length],color:r(t.progress)}}function a(e){let t=e.slice(0,5),n=e.length-t.length,r=n>0?`+${n}`:``;return{text:`${t.map(e=>e.glyph).join(``)}${r}`,segments:[...t.map(e=>({text:e.glyph,...e.color?{color:e.color}:{}})),...r?[{text:r,color:`dim`}]:[]]}}function o(e,t,n){let r=[{text:n,color:`accent`}],i=n;if(t.length>0){let e=a(t);i+=e.text,r.push(...e.segments)}if(e.failed>0){let t=`${i.endsWith(n)?``:` `}⚠${e.failed}`;i+=t,r.push({text:t,color:`warning`})}return{text:i,segments:r}}function s(e,t=0){if(e.runs.length===0&&e.failed===0)return;let n=e.runs.map(e=>i(e,t)),r=o(e,n,`workflow: `),a=o(e,n,`W `),s=e.runs.some(e=>e.state===`running`);return{text:r.text,footer:{fullText:r.text,compactText:a.text,fullSegments:r.segments,compactSegments:a.segments},fingerprint:`${e.runs.map((e,t)=>`${e.key}:${e.state}:${n[t]?.color}`).join(`|`)}:${e.failed}:${s?t:`static`}`,pulsing:s}}exports.WORKFLOW_PULSE_FRAMES=e,exports.workflowFooterStatus=s;
2
+ //# sourceMappingURL=workflowFooterStatus.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowFooterStatus.cjs","names":[],"sources":["../../src/workflow/workflowFooterStatus.ts"],"sourcesContent":["/**\n * The workflow footer contribution: one dot per run this session has going.\n *\n * Replaces the full-width progress row that used to sit above the editor. That\n * row cost a terminal line per repaint and grew with the run count; a footer\n * group costs nothing when idle and stays one glance wide however many runs are\n * in flight, which is the same trade doom-team's agent dots make.\n *\n * Pure: takes already-summarised runs and a frame number, returns text and\n * segments. No registry, no TUI, no timer.\n */\n\nimport type { DoomFooterContributionValue, FooterTextSegment } from '@agimon-ai/doompi-extension-contracts/footer';\n\n/** Same three-phase pulse doom-team uses, so every doom footer animates alike. */\nexport const WORKFLOW_PULSE_FRAMES = ['◐', '●', '◑'] as const;\n\n/** House `name: state` shape, matching plan mode and the doom loop. */\nconst FULL_PREFIX = 'workflow: ';\nconst COMPACT_PREFIX = 'W ';\n/**\n * Dots past this collapse into `+N`. The footer is one line shared with every\n * other extension, so a group that grows with the run count is the group that\n * pushes everyone else off the end.\n */\nconst MAX_DOTS = 5;\nconst PAUSED_GLYPH = '■';\nconst STARTING_GLYPH = '○';\nconst FAILED_GLYPH = '⚠';\n\n/**\n * Cold-to-done ramp for a working run, walked by job completion.\n *\n * `warning` is deliberately absent: it already means paused and failed, and a\n * two-thirds-done run must not read as a run needing attention. Blue, cyan and\n * light-green in the doom theme, all members of the footer colour union.\n */\nconst PROGRESS_COLORS = ['accent', 'mdCode', 'toolDiffAdded'] as const satisfies readonly FooterTextSegment['color'][];\n\nexport type WorkflowDotState = 'running' | 'paused' | 'starting';\n\n/** One run's contribution to the row: its identity and what it is doing. */\nexport interface WorkflowFooterRun {\n key: string;\n state: WorkflowDotState;\n /**\n * Jobs finished over jobs total, 0..1.\n *\n * Optional because the position only rides on job-start events: a run whose\n * engine never stamped one keeps the flat first-bucket colour rather than\n * claiming a progress it cannot know.\n */\n progress?: number;\n}\n\nexport interface WorkflowFooterInput {\n runs: readonly WorkflowFooterRun[];\n failed: number;\n}\n\nexport interface WorkflowFooterStatus {\n text: string;\n footer: DoomFooterContributionValue;\n /** Changes exactly when the rendered row would, so repaints can be skipped. */\n fingerprint: string;\n /** True while at least one run is animating, which is what the ticker keys off. */\n pulsing: boolean;\n}\n\ninterface Dot {\n glyph: string;\n color: FooterTextSegment['color'];\n}\n\n/**\n * Per-run phase offset so a fleet of runs does not blink in lockstep, which\n * reads as one flashing indicator rather than several working ones.\n */\nfunction stableOffset(value: string): number {\n let hash = 0;\n for (const character of value) hash = (Math.imul(hash, 31) + (character.codePointAt(0) ?? 0)) >>> 0;\n return hash % WORKFLOW_PULSE_FRAMES.length;\n}\n\n/**\n * Where a run sits on the ramp.\n *\n * Bucketed rather than interpolated because the ramp has three colours and a\n * dot has one: a reader is being asked \"early, middle or nearly done\", which is\n * the most a single glyph can honestly answer.\n */\nfunction progressColor(progress: number | undefined): Dot['color'] {\n if (progress === undefined || !Number.isFinite(progress)) return PROGRESS_COLORS[0];\n const bucket = Math.floor(Math.max(0, Math.min(1, progress)) * PROGRESS_COLORS.length);\n return PROGRESS_COLORS[Math.min(bucket, PROGRESS_COLORS.length - 1)];\n}\n\nfunction dotFor(run: WorkflowFooterRun, frame: number): Dot {\n if (run.state === 'paused') return { glyph: PAUSED_GLYPH, color: 'warning' };\n if (run.state === 'starting') return { glyph: STARTING_GLYPH, color: 'dim' };\n return {\n glyph: WORKFLOW_PULSE_FRAMES[(frame + stableOffset(run.key)) % WORKFLOW_PULSE_FRAMES.length],\n color: progressColor(run.progress),\n };\n}\n\n/** Dots up to the cap, then the count of everything the cap hid. */\nfunction projectDots(dots: readonly Dot[]): { text: string; segments: FooterTextSegment[] } {\n const visible = dots.slice(0, MAX_DOTS);\n const hidden = dots.length - visible.length;\n const suffix = hidden > 0 ? `+${hidden}` : '';\n return {\n text: `${visible.map((dot) => dot.glyph).join('')}${suffix}`,\n segments: [\n ...visible.map((dot) => ({ text: dot.glyph, ...(dot.color ? { color: dot.color } : {}) })),\n ...(suffix ? [{ text: suffix, color: 'dim' as const }] : []),\n ],\n };\n}\n\nfunction project(\n input: WorkflowFooterInput,\n dots: readonly Dot[],\n prefix: string,\n): {\n text: string;\n segments: FooterTextSegment[];\n} {\n const segments: FooterTextSegment[] = [{ text: prefix, color: 'accent' }];\n let text = prefix;\n\n if (dots.length > 0) {\n const projected = projectDots(dots);\n text += projected.text;\n segments.push(...projected.segments);\n }\n\n if (input.failed > 0) {\n const failed = `${text.endsWith(prefix) ? '' : ' '}${FAILED_GLYPH}${input.failed}`;\n text += failed;\n segments.push({ text: failed, color: 'warning' });\n }\n return { text, segments };\n}\n\n/**\n * Render one animation frame, or nothing at all.\n *\n * An absent value removes the group entirely rather than leaving an empty\n * `workflow:` label behind on an idle session.\n */\nexport function workflowFooterStatus(input: WorkflowFooterInput, frame = 0): WorkflowFooterStatus | undefined {\n if (input.runs.length === 0 && input.failed === 0) return undefined;\n\n const dots = input.runs.map((run) => dotFor(run, frame));\n const full = project(input, dots, FULL_PREFIX);\n const compact = project(input, dots, COMPACT_PREFIX);\n const pulsing = input.runs.some((run) => run.state === 'running');\n return {\n text: full.text,\n footer: {\n fullText: full.text,\n compactText: compact.text,\n fullSegments: full.segments,\n compactSegments: compact.segments,\n },\n // Keyed on the resolved colour, not on the raw progress: a run inching\n // along inside one bucket changes nothing on screen and must not repaint.\n fingerprint: `${input.runs.map((run, index) => `${run.key}:${run.state}:${dots[index]?.color}`).join('|')}:${input.failed}:${pulsing ? frame : 'static'}`,\n pulsing,\n };\n}\n"],"mappings":"AAeA,MAAa,EAAwB,CAAC,IAAK,IAAK,GAAG,EAsB7C,EAAkB,CAAC,SAAU,SAAU,eAAe,EAyC5D,SAAS,EAAa,EAAuB,CAC3C,IAAI,EAAO,EACX,IAAK,IAAM,KAAa,EAAO,EAAQ,KAAK,KAAK,EAAM,EAAE,GAAK,EAAU,YAAY,CAAC,GAAK,KAAQ,EAClG,OAAO,EAAO,EAAsB,MACtC,CASA,SAAS,EAAc,EAA4C,CACjE,GAAI,IAAa,IAAA,IAAa,CAAC,OAAO,SAAS,CAAQ,EAAG,OAAO,EAAgB,GACjF,IAAM,EAAS,KAAK,MAAM,KAAK,IAAI,EAAG,KAAK,IAAI,EAAG,CAAQ,CAAC,EAAI,EAAgB,MAAM,EACrF,OAAO,EAAgB,KAAK,IAAI,EAAQ,EAAgB,OAAS,CAAC,EACpE,CAEA,SAAS,EAAO,EAAwB,EAAoB,CAG1D,OAFI,EAAI,QAAU,SAAiB,CAAE,MAAO,IAAc,MAAO,SAAU,EACvE,EAAI,QAAU,WAAmB,CAAE,MAAO,IAAgB,MAAO,KAAM,EACpE,CACL,MAAO,GAAuB,EAAQ,EAAa,EAAI,GAAG,GAAK,EAAsB,QACrF,MAAO,EAAc,EAAI,QAAQ,CACnC,CACF,CAGA,SAAS,EAAY,EAAuE,CAC1F,IAAM,EAAU,EAAK,MAAM,EAAG,CAAQ,EAChC,EAAS,EAAK,OAAS,EAAQ,OAC/B,EAAS,EAAS,EAAI,IAAI,IAAW,GAC3C,MAAO,CACL,KAAM,GAAG,EAAQ,IAAK,GAAQ,EAAI,KAAK,CAAC,CAAC,KAAK,EAAE,IAAI,IACpD,SAAU,CACR,GAAG,EAAQ,IAAK,IAAS,CAAE,KAAM,EAAI,MAAO,GAAI,EAAI,MAAQ,CAAE,MAAO,EAAI,KAAM,EAAI,CAAC,CAAG,EAAE,EACzF,GAAI,EAAS,CAAC,CAAE,KAAM,EAAQ,MAAO,KAAe,CAAC,EAAI,CAAC,CAC5D,CACF,CACF,CAEA,SAAS,EACP,EACA,EACA,EAIA,CACA,IAAM,EAAgC,CAAC,CAAE,KAAM,EAAQ,MAAO,QAAS,CAAC,EACpE,EAAO,EAEX,GAAI,EAAK,OAAS,EAAG,CACnB,IAAM,EAAY,EAAY,CAAI,EAClC,GAAQ,EAAU,KAClB,EAAS,KAAK,GAAG,EAAU,QAAQ,CACrC,CAEA,GAAI,EAAM,OAAS,EAAG,CACpB,IAAM,EAAS,GAAG,EAAK,SAAS,CAAM,EAAI,GAAK,OAAqB,EAAM,SAC1E,GAAQ,EACR,EAAS,KAAK,CAAE,KAAM,EAAQ,MAAO,SAAU,CAAC,CAClD,CACA,MAAO,CAAE,OAAM,UAAS,CAC1B,CAQA,SAAgB,EAAqB,EAA4B,EAAQ,EAAqC,CAC5G,GAAI,EAAM,KAAK,SAAW,GAAK,EAAM,SAAW,EAAG,OAEnD,IAAM,EAAO,EAAM,KAAK,IAAK,GAAQ,EAAO,EAAK,CAAK,CAAC,EACjD,EAAO,EAAQ,EAAO,EAAM,YAAW,EACvC,EAAU,EAAQ,EAAO,EAAM,IAAc,EAC7C,EAAU,EAAM,KAAK,KAAM,GAAQ,EAAI,QAAU,SAAS,EAChE,MAAO,CACL,KAAM,EAAK,KACX,OAAQ,CACN,SAAU,EAAK,KACf,YAAa,EAAQ,KACrB,aAAc,EAAK,SACnB,gBAAiB,EAAQ,QAC3B,EAGA,YAAa,GAAG,EAAM,KAAK,KAAK,EAAK,IAAU,GAAG,EAAI,IAAI,GAAG,EAAI,MAAM,GAAG,EAAK,EAAM,EAAE,OAAO,CAAC,CAAC,KAAK,GAAG,EAAE,GAAG,EAAM,OAAO,GAAG,EAAU,EAAQ,WAC/I,SACF,CACF"}
@@ -0,0 +1,2 @@
1
+ const e=[`◐`,`●`,`◑`],t=[`accent`,`mdCode`,`toolDiffAdded`];function n(t){let n=0;for(let e of t)n=Math.imul(n,31)+(e.codePointAt(0)??0)>>>0;return n%e.length}function r(e){if(e===void 0||!Number.isFinite(e))return t[0];let n=Math.floor(Math.max(0,Math.min(1,e))*t.length);return t[Math.min(n,t.length-1)]}function i(t,i){return t.state===`paused`?{glyph:`■`,color:`warning`}:t.state===`starting`?{glyph:`○`,color:`dim`}:{glyph:e[(i+n(t.key))%e.length],color:r(t.progress)}}function a(e){let t=e.slice(0,5),n=e.length-t.length,r=n>0?`+${n}`:``;return{text:`${t.map(e=>e.glyph).join(``)}${r}`,segments:[...t.map(e=>({text:e.glyph,...e.color?{color:e.color}:{}})),...r?[{text:r,color:`dim`}]:[]]}}function o(e,t,n){let r=[{text:n,color:`accent`}],i=n;if(t.length>0){let e=a(t);i+=e.text,r.push(...e.segments)}if(e.failed>0){let t=`${i.endsWith(n)?``:` `}⚠${e.failed}`;i+=t,r.push({text:t,color:`warning`})}return{text:i,segments:r}}function s(e,t=0){if(e.runs.length===0&&e.failed===0)return;let n=e.runs.map(e=>i(e,t)),r=o(e,n,`workflow: `),a=o(e,n,`W `),s=e.runs.some(e=>e.state===`running`);return{text:r.text,footer:{fullText:r.text,compactText:a.text,fullSegments:r.segments,compactSegments:a.segments},fingerprint:`${e.runs.map((e,t)=>`${e.key}:${e.state}:${n[t]?.color}`).join(`|`)}:${e.failed}:${s?t:`static`}`,pulsing:s}}export{e as WORKFLOW_PULSE_FRAMES,s as workflowFooterStatus};
2
+ //# sourceMappingURL=workflowFooterStatus.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowFooterStatus.mjs","names":[],"sources":["../../src/workflow/workflowFooterStatus.ts"],"sourcesContent":["/**\n * The workflow footer contribution: one dot per run this session has going.\n *\n * Replaces the full-width progress row that used to sit above the editor. That\n * row cost a terminal line per repaint and grew with the run count; a footer\n * group costs nothing when idle and stays one glance wide however many runs are\n * in flight, which is the same trade doom-team's agent dots make.\n *\n * Pure: takes already-summarised runs and a frame number, returns text and\n * segments. No registry, no TUI, no timer.\n */\n\nimport type { DoomFooterContributionValue, FooterTextSegment } from '@agimon-ai/doompi-extension-contracts/footer';\n\n/** Same three-phase pulse doom-team uses, so every doom footer animates alike. */\nexport const WORKFLOW_PULSE_FRAMES = ['◐', '●', '◑'] as const;\n\n/** House `name: state` shape, matching plan mode and the doom loop. */\nconst FULL_PREFIX = 'workflow: ';\nconst COMPACT_PREFIX = 'W ';\n/**\n * Dots past this collapse into `+N`. The footer is one line shared with every\n * other extension, so a group that grows with the run count is the group that\n * pushes everyone else off the end.\n */\nconst MAX_DOTS = 5;\nconst PAUSED_GLYPH = '■';\nconst STARTING_GLYPH = '○';\nconst FAILED_GLYPH = '⚠';\n\n/**\n * Cold-to-done ramp for a working run, walked by job completion.\n *\n * `warning` is deliberately absent: it already means paused and failed, and a\n * two-thirds-done run must not read as a run needing attention. Blue, cyan and\n * light-green in the doom theme, all members of the footer colour union.\n */\nconst PROGRESS_COLORS = ['accent', 'mdCode', 'toolDiffAdded'] as const satisfies readonly FooterTextSegment['color'][];\n\nexport type WorkflowDotState = 'running' | 'paused' | 'starting';\n\n/** One run's contribution to the row: its identity and what it is doing. */\nexport interface WorkflowFooterRun {\n key: string;\n state: WorkflowDotState;\n /**\n * Jobs finished over jobs total, 0..1.\n *\n * Optional because the position only rides on job-start events: a run whose\n * engine never stamped one keeps the flat first-bucket colour rather than\n * claiming a progress it cannot know.\n */\n progress?: number;\n}\n\nexport interface WorkflowFooterInput {\n runs: readonly WorkflowFooterRun[];\n failed: number;\n}\n\nexport interface WorkflowFooterStatus {\n text: string;\n footer: DoomFooterContributionValue;\n /** Changes exactly when the rendered row would, so repaints can be skipped. */\n fingerprint: string;\n /** True while at least one run is animating, which is what the ticker keys off. */\n pulsing: boolean;\n}\n\ninterface Dot {\n glyph: string;\n color: FooterTextSegment['color'];\n}\n\n/**\n * Per-run phase offset so a fleet of runs does not blink in lockstep, which\n * reads as one flashing indicator rather than several working ones.\n */\nfunction stableOffset(value: string): number {\n let hash = 0;\n for (const character of value) hash = (Math.imul(hash, 31) + (character.codePointAt(0) ?? 0)) >>> 0;\n return hash % WORKFLOW_PULSE_FRAMES.length;\n}\n\n/**\n * Where a run sits on the ramp.\n *\n * Bucketed rather than interpolated because the ramp has three colours and a\n * dot has one: a reader is being asked \"early, middle or nearly done\", which is\n * the most a single glyph can honestly answer.\n */\nfunction progressColor(progress: number | undefined): Dot['color'] {\n if (progress === undefined || !Number.isFinite(progress)) return PROGRESS_COLORS[0];\n const bucket = Math.floor(Math.max(0, Math.min(1, progress)) * PROGRESS_COLORS.length);\n return PROGRESS_COLORS[Math.min(bucket, PROGRESS_COLORS.length - 1)];\n}\n\nfunction dotFor(run: WorkflowFooterRun, frame: number): Dot {\n if (run.state === 'paused') return { glyph: PAUSED_GLYPH, color: 'warning' };\n if (run.state === 'starting') return { glyph: STARTING_GLYPH, color: 'dim' };\n return {\n glyph: WORKFLOW_PULSE_FRAMES[(frame + stableOffset(run.key)) % WORKFLOW_PULSE_FRAMES.length],\n color: progressColor(run.progress),\n };\n}\n\n/** Dots up to the cap, then the count of everything the cap hid. */\nfunction projectDots(dots: readonly Dot[]): { text: string; segments: FooterTextSegment[] } {\n const visible = dots.slice(0, MAX_DOTS);\n const hidden = dots.length - visible.length;\n const suffix = hidden > 0 ? `+${hidden}` : '';\n return {\n text: `${visible.map((dot) => dot.glyph).join('')}${suffix}`,\n segments: [\n ...visible.map((dot) => ({ text: dot.glyph, ...(dot.color ? { color: dot.color } : {}) })),\n ...(suffix ? [{ text: suffix, color: 'dim' as const }] : []),\n ],\n };\n}\n\nfunction project(\n input: WorkflowFooterInput,\n dots: readonly Dot[],\n prefix: string,\n): {\n text: string;\n segments: FooterTextSegment[];\n} {\n const segments: FooterTextSegment[] = [{ text: prefix, color: 'accent' }];\n let text = prefix;\n\n if (dots.length > 0) {\n const projected = projectDots(dots);\n text += projected.text;\n segments.push(...projected.segments);\n }\n\n if (input.failed > 0) {\n const failed = `${text.endsWith(prefix) ? '' : ' '}${FAILED_GLYPH}${input.failed}`;\n text += failed;\n segments.push({ text: failed, color: 'warning' });\n }\n return { text, segments };\n}\n\n/**\n * Render one animation frame, or nothing at all.\n *\n * An absent value removes the group entirely rather than leaving an empty\n * `workflow:` label behind on an idle session.\n */\nexport function workflowFooterStatus(input: WorkflowFooterInput, frame = 0): WorkflowFooterStatus | undefined {\n if (input.runs.length === 0 && input.failed === 0) return undefined;\n\n const dots = input.runs.map((run) => dotFor(run, frame));\n const full = project(input, dots, FULL_PREFIX);\n const compact = project(input, dots, COMPACT_PREFIX);\n const pulsing = input.runs.some((run) => run.state === 'running');\n return {\n text: full.text,\n footer: {\n fullText: full.text,\n compactText: compact.text,\n fullSegments: full.segments,\n compactSegments: compact.segments,\n },\n // Keyed on the resolved colour, not on the raw progress: a run inching\n // along inside one bucket changes nothing on screen and must not repaint.\n fingerprint: `${input.runs.map((run, index) => `${run.key}:${run.state}:${dots[index]?.color}`).join('|')}:${input.failed}:${pulsing ? frame : 'static'}`,\n pulsing,\n };\n}\n"],"mappings":"AAeA,MAAa,EAAwB,CAAC,IAAK,IAAK,GAAG,EAsB7C,EAAkB,CAAC,SAAU,SAAU,eAAe,EAyC5D,SAAS,EAAa,EAAuB,CAC3C,IAAI,EAAO,EACX,IAAK,IAAM,KAAa,EAAO,EAAQ,KAAK,KAAK,EAAM,EAAE,GAAK,EAAU,YAAY,CAAC,GAAK,KAAQ,EAClG,OAAO,EAAO,EAAsB,MACtC,CASA,SAAS,EAAc,EAA4C,CACjE,GAAI,IAAa,IAAA,IAAa,CAAC,OAAO,SAAS,CAAQ,EAAG,OAAO,EAAgB,GACjF,IAAM,EAAS,KAAK,MAAM,KAAK,IAAI,EAAG,KAAK,IAAI,EAAG,CAAQ,CAAC,EAAI,EAAgB,MAAM,EACrF,OAAO,EAAgB,KAAK,IAAI,EAAQ,EAAgB,OAAS,CAAC,EACpE,CAEA,SAAS,EAAO,EAAwB,EAAoB,CAG1D,OAFI,EAAI,QAAU,SAAiB,CAAE,MAAO,IAAc,MAAO,SAAU,EACvE,EAAI,QAAU,WAAmB,CAAE,MAAO,IAAgB,MAAO,KAAM,EACpE,CACL,MAAO,GAAuB,EAAQ,EAAa,EAAI,GAAG,GAAK,EAAsB,QACrF,MAAO,EAAc,EAAI,QAAQ,CACnC,CACF,CAGA,SAAS,EAAY,EAAuE,CAC1F,IAAM,EAAU,EAAK,MAAM,EAAG,CAAQ,EAChC,EAAS,EAAK,OAAS,EAAQ,OAC/B,EAAS,EAAS,EAAI,IAAI,IAAW,GAC3C,MAAO,CACL,KAAM,GAAG,EAAQ,IAAK,GAAQ,EAAI,KAAK,CAAC,CAAC,KAAK,EAAE,IAAI,IACpD,SAAU,CACR,GAAG,EAAQ,IAAK,IAAS,CAAE,KAAM,EAAI,MAAO,GAAI,EAAI,MAAQ,CAAE,MAAO,EAAI,KAAM,EAAI,CAAC,CAAG,EAAE,EACzF,GAAI,EAAS,CAAC,CAAE,KAAM,EAAQ,MAAO,KAAe,CAAC,EAAI,CAAC,CAC5D,CACF,CACF,CAEA,SAAS,EACP,EACA,EACA,EAIA,CACA,IAAM,EAAgC,CAAC,CAAE,KAAM,EAAQ,MAAO,QAAS,CAAC,EACpE,EAAO,EAEX,GAAI,EAAK,OAAS,EAAG,CACnB,IAAM,EAAY,EAAY,CAAI,EAClC,GAAQ,EAAU,KAClB,EAAS,KAAK,GAAG,EAAU,QAAQ,CACrC,CAEA,GAAI,EAAM,OAAS,EAAG,CACpB,IAAM,EAAS,GAAG,EAAK,SAAS,CAAM,EAAI,GAAK,OAAqB,EAAM,SAC1E,GAAQ,EACR,EAAS,KAAK,CAAE,KAAM,EAAQ,MAAO,SAAU,CAAC,CAClD,CACA,MAAO,CAAE,OAAM,UAAS,CAC1B,CAQA,SAAgB,EAAqB,EAA4B,EAAQ,EAAqC,CAC5G,GAAI,EAAM,KAAK,SAAW,GAAK,EAAM,SAAW,EAAG,OAEnD,IAAM,EAAO,EAAM,KAAK,IAAK,GAAQ,EAAO,EAAK,CAAK,CAAC,EACjD,EAAO,EAAQ,EAAO,EAAM,YAAW,EACvC,EAAU,EAAQ,EAAO,EAAM,IAAc,EAC7C,EAAU,EAAM,KAAK,KAAM,GAAQ,EAAI,QAAU,SAAS,EAChE,MAAO,CACL,KAAM,EAAK,KACX,OAAQ,CACN,SAAU,EAAK,KACf,YAAa,EAAQ,KACrB,aAAc,EAAK,SACnB,gBAAiB,EAAQ,QAC3B,EAGA,YAAa,GAAG,EAAM,KAAK,KAAK,EAAK,IAAU,GAAG,EAAI,IAAI,GAAG,EAAI,MAAM,GAAG,EAAK,EAAM,EAAE,OAAO,CAAC,CAAC,KAAK,GAAG,EAAE,GAAG,EAAM,OAAO,GAAG,EAAU,EAAQ,WAC/I,SACF,CACF"}
@@ -0,0 +1,2 @@
1
+ const e=require("./doomOverlay.cjs"),t=require("./workflowStatusRow.cjs");let n=require("@earendil-works/pi-tui"),r=require("@agimon-ai/workflow-mcp");e.DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;const i=`running`,a=`paused`,o=[[`↑↓/jk`,`select`],[`PgUp/PgDn`,`page`],[`enter`,`open`],[`esc`,`close`]];function s(e,t){let r=(0,n.truncateToWidth)(e,Math.max(0,t),`…`).replaceAll(`\x1B[0m`,``);return r+` `.repeat(Math.max(0,t-(0,n.visibleWidth)(r)))}function c(e,t,r){let i=(0,n.visibleWidth)(t),a=Math.max(0,r-i-1);return s(e,a)+` `.repeat(Math.max(1,r-a-i))+s(t,i)}function l(e){return e===`completed`?`✓`:e===`failed`?`✗`:`●`}function u(e){return e===`completed`?`success`:e===`failed`?`error`:`accent`}function d(e,t,r){let a=[];for(let o of e.jobs){let e=o.total?` · ${(o.index??0)+1}/${o.total}`:``;if(a.push((0,n.truncateToWidth)(r.fg(u(o.status),`${l(o.status)} ${o.name}`)+r.fg(`dim`,e),t,`…`)),o.status===i)for(let e of o.steps){let o=e.status===`failed`?`error`:e.status===i?`muted`:`dim`;a.push((0,n.truncateToWidth)(r.fg(o,` ${l(e.status)} ${e.name}`),t,`…`))}}return a}function f(e,o,s,l){if(!e)return[l.fg(`dim`,`Select a workflow to inspect its jobs and output.`)];let u=(0,r.currentWorkflowPosition)(e.jobs),f=t.humanizeDuration(Math.max(0,Date.now()-Date.parse(e.startedAt))),p=e.executionState??i,m=p===a?`warning`:`dim`,h=[c(`${l.fg(`accent`,`●`)} ${l.bold(e.displayName)}`,l.fg(m,p),o),(0,n.truncateToWidth)(l.fg(`dim`,`${e.runKey} · ${e.workspace} · ${f}`),o,`…`),u?(0,n.truncateToWidth)(`${l.fg(`accent`,u.job)}${u.step?l.fg(`muted`,` · ${u.step}`):``}`,o,`…`):l.fg(`dim`,`starting`),``,l.fg(`dim`,`PROGRESS`)],g=e.output.length>0?3:0,_=Math.max(0,s-h.length-g);if(h.push(...d(e,o,l).slice(0,_)),e.output.length>0&&s-h.length>=2){h.push(``,l.fg(`dim`,`OUTPUT · newest last`));let t=Math.max(0,s-h.length);h.push(...e.output.slice(-t).map(e=>(0,n.truncateToWidth)(e,o,`…`)))}return h.slice(0,s)}var p=class extends e.DoomOverlay{source;done;bodyHeight=8;completed=!1;error;items=[];refreshActive=!1;selected=0;selectedKey;timer;constructor(e,t,n,r,i={}){super(e,t),this.source=n,this.done=r,this.selectedKey=i.initialKey,this.refresh(),this.timer=setInterval(()=>void this.refresh(),i.refreshMs??750),this.timer.unref?.()}async refresh(){if(!(this.refreshActive||this.completed)){this.refreshActive=!0;try{let e=this.items[this.selected]?.key??this.selectedKey,t=await this.source.list();if(this.completed)return;let n=e?t.findIndex(t=>t.key===e):-1;this.items=t,this.selected=n>=0?n:Math.min(this.selected,Math.max(0,t.length-1)),this.selectedKey=t[this.selected]?.key;let r=t[this.selected];r&&(r.output=await this.source.output(r)),this.error=void 0,this.completed||this.tui.requestRender()}catch(e){this.error=e instanceof Error?e.message:String(e),this.completed||this.tui.requestRender()}finally{this.refreshActive=!1}}}move(e){this.items.length!==0&&(this.selected=Math.max(0,Math.min(this.items.length-1,this.selected+e)),this.selectedKey=this.items[this.selected]?.key,this.refreshSelectedOutput(),this.tui.requestRender())}async refreshSelectedOutput(){let e=this.items[this.selected];if(!(!e||this.completed)){try{e.output=await this.source.output(e),this.error=void 0}catch(e){this.error=e instanceof Error?e.message:String(e)}!this.completed&&e.key===this.selectedKey&&this.tui.requestRender()}}finish(e){this.completed||(this.completed=!0,clearInterval(this.timer),this.done(e))}close(){this.finish(void 0)}handleInput(e){if((0,n.matchesKey)(e,`escape`)||(0,n.matchesKey)(e,`ctrl+c`)){this.finish(void 0);return}if((0,n.matchesKey)(e,`up`)||e===`k`){this.move(-1);return}if((0,n.matchesKey)(e,`down`)||e===`j`){this.move(1);return}if((0,n.matchesKey)(e,`pageUp`)){this.move(-this.rosterCapacity());return}if((0,n.matchesKey)(e,`pageDown`)){this.move(this.rosterCapacity());return}if((0,n.matchesKey)(e,`enter`)){let e=this.items[this.selected];e&&this.finish({...e.runId?{runId:e.runId}:{},runKey:e.runKey,startedAt:e.startedAt,workspace:e.workspace})}}rosterCapacity(){return Math.max(1,Math.floor((this.bodyHeight-2)/2))}current(){if(this.items.length!==0)return this.items[Math.min(this.selected,this.items.length-1)]}getChrome(){let t=this.current(),n=t?`${t.displayName} · ${t.executionState??i}`:`no active workflows`;return{title:`WORKFLOW SPACE`,accent:e.DOOM_OVERLAY_ACCENT,breadcrumb:`SPC › w / workflows › l / manage`,headerRight:`${this.items.length} running · ${n}`,footer:`↑↓/jk select · PgUp/PgDn page · enter open · esc close`,footerHints:o,footerRight:this.items.length>0?`${Math.min(this.selected,this.items.length-1)+1}/${this.items.length}`:`empty`}}renderBody(e,t){this.bodyHeight=t;let r=e=>this.error?[this.theme.fg(`warning`,`Unable to refresh workflows: ${this.error}`)]:f(this.current(),e,t,this.theme);if(e<48)return r(e).slice(0,t);let i=Math.max(18,Math.floor((e-1)*.3333333333333333)),a=Math.max(1,e-i-1),o=Math.max(1,i-1),c=Math.max(1,a-1),l=this.rosterLines(o),u=r(c),d=this.theme.fg(`borderMuted`,`│`);return Array.from({length:t},(t,r)=>{let i=`${s(l[r]??``,o)} ${d} ${s(u[r]??``,c)}`;return(0,n.truncateToWidth)(i,e,`…`)})}rosterLines(e){let t=[this.theme.bold(`RUNNING · this session`),``];if(this.items.length===0)return t.push(this.theme.fg(`dim`,`No active workflows`)),t;let n=this.rosterCapacity(),i=Math.min(this.selected,this.items.length-1),o=Math.max(0,Math.min(i-n+1,Math.max(0,this.items.length-n)));for(let[c,l]of this.items.slice(o,o+n).entries()){let n=o+c===i,u=l.executionState===a,d=n?this.theme.fg(`accent`,`›`):` `,f=this.theme.fg(u?`warning`:`accent`,`●`),p=n?this.theme.bold(l.displayName):l.displayName,m=u?a:(0,r.currentWorkflowPosition)(l.jobs)?.job??`starting`,h=`${d} ${f} ${p}`,g=`${` `.repeat(4)}${this.theme.fg(u?`warning`:`dim`,m)}`,_=n?`selectedBg`:`userMessageBg`;for(let n of[h,g])t.push(this.theme.bg(_,s(n,e)))}return t}invalidate(){this.refresh()}dispose(){this.completed||(this.completed=!0,clearInterval(this.timer))}};exports.WorkflowInspectorComponent=p;
2
+ //# sourceMappingURL=workflowInspector.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowInspector.cjs","names":["DOOM_FULLSCREEN_UI_OPTIONS","truncateToWidth","visibleWidth","currentWorkflowPosition","humanizeDuration","DoomOverlay","matchesKey","DOOM_OVERLAY_ACCENT"],"sources":["../../src/workflow/workflowInspector.ts"],"sourcesContent":["/**\n * Workflow Space: the `SPC w l` overlay.\n *\n * A doom overlay rather than a hand-rolled frame: the roster owns the left\n * third, the run under the cursor owns the right two thirds, and the chrome is\n * the same frame, breadcrumb and key legend every other doom surface draws.\n *\n * Rendering is pure -- `renderBody(width, height)` reads the last polled\n * snapshot and returns lines with no side effects -- so the surface can be\n * asserted as text without a live terminal.\n */\n\nimport type { Theme } from '@earendil-works/pi-coding-agent';\nimport {\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n DOOM_FULLSCREEN_UI_OPTIONS,\n} from './doomOverlay.ts';\nimport { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport { currentWorkflowPosition, type WorkflowProgressJob } from '@agimon-ai/workflow-mcp';\nimport { humanizeDuration } from './workflowStatusRow';\n\nexport const WORKFLOW_INSPECTOR_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nexport interface WorkflowInspectorSelection {\n runId?: string;\n runKey: string;\n startedAt: string;\n workspace: string;\n}\n\nexport interface WorkflowInspectorItem extends WorkflowInspectorSelection {\n displayName: string;\n executionState?: string;\n jobs: WorkflowProgressJob[];\n key: string;\n output: string[];\n}\n\nexport interface WorkflowInspectorSource {\n list(): Promise<WorkflowInspectorItem[]>;\n output(item: WorkflowInspectorItem): Promise<string[]>;\n}\n\nexport interface WorkflowInspectorOptions {\n initialKey?: string;\n refreshMs?: number;\n}\n\nconst TITLE = 'WORKFLOW SPACE';\nconst BREADCRUMB = 'SPC › w / workflows › l / manage';\nconst ROSTER_HEADING = 'RUNNING · this session';\nconst EMPTY_ROSTER = 'No active workflows';\nconst PROGRESS_HEADING = 'PROGRESS';\nconst OUTPUT_HEADING = 'OUTPUT · newest last';\nconst NO_SELECTION = 'Select a workflow to inspect its jobs and output.';\nconst RUNNING_STATE = 'running';\nconst PAUSED_STATE = 'paused';\nconst FOOTER = '↑↓/jk select · PgUp/PgDn page · enter open · esc close';\nconst FOOTER_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓/jk', 'select'],\n ['PgUp/PgDn', 'page'],\n ['enter', 'open'],\n ['esc', 'close'],\n];\n\nconst DEFAULT_REFRESH_MS = 750;\nconst SELECTION_MARKER = '›';\nconst ELLIPSIS = '…';\n/** pi-tui brackets a truncation ellipsis with this; see `fit`. */\nconst HARD_RESET = '\\x1b[0m';\n/** A third for the roster, two thirds for the run being read. */\nconst LIST_PANE_RATIO = 1 / 3;\n/** One blank column each side of the divider so neither pane touches it. */\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 18;\n/** Below this the roster cannot hold a readable name, so the detail takes the body. */\nconst MIN_TWO_PANE_WIDTH = 48;\n/** Each run occupies a name row and a state row beneath it. */\nconst ROWS_PER_ITEM = 2;\n/** Hanging indent for the state row: clears the marker and the glyph. */\nconst META_INDENT = 4;\n/** Rows the roster heading and its spacer take. */\nconst ROSTER_CHROME_ROWS = 2;\n/** The output tail keeps at least this many rows, or it is not worth a divider. */\nconst MIN_OUTPUT_ROWS = 2;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\n/**\n * Truncates and pads to an exact column count.\n *\n * `truncateToWidth` wraps its ellipsis in a hard `\\x1b[0m`, which would strip a\n * row's background for everything after a clipped name. The theme closes its own\n * colours with `\\x1b[39m` and `\\x1b[49m`, so the injected resets are dropped.\n */\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS).replaceAll(HARD_RESET, '');\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction rightAligned(left: string, right: string, width: number): string {\n const rightWidth = visibleWidth(right);\n const leftWidth = Math.max(0, width - rightWidth - 1);\n return fit(left, leftWidth) + ' '.repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);\n}\n\nfunction jobGlyph(status: string): string {\n return status === 'completed' ? '✓' : status === 'failed' ? '✗' : '●';\n}\n\nfunction jobColour(status: string): ThemeColor {\n return status === 'completed' ? 'success' : status === 'failed' ? 'error' : 'accent';\n}\n\nfunction progressLines(item: WorkflowInspectorItem, width: number, theme: Theme): string[] {\n const lines: string[] = [];\n for (const job of item.jobs) {\n const position = job.total ? ` · ${(job.index ?? 0) + 1}/${job.total}` : '';\n lines.push(\n truncateToWidth(\n theme.fg(jobColour(job.status), `${jobGlyph(job.status)} ${job.name}`) + theme.fg('dim', position),\n width,\n ELLIPSIS,\n ),\n );\n // Only the running job's steps: every finished job's steps would push the\n // live one off the pane, which is the one thing worth watching.\n if (job.status !== RUNNING_STATE) continue;\n for (const step of job.steps) {\n const stepColour: ThemeColor =\n step.status === 'failed' ? 'error' : step.status === RUNNING_STATE ? 'muted' : 'dim';\n lines.push(truncateToWidth(theme.fg(stepColour, ` ${jobGlyph(step.status)} ${step.name}`), width, ELLIPSIS));\n }\n }\n return lines;\n}\n\nfunction detailLines(item: WorkflowInspectorItem | undefined, width: number, height: number, theme: Theme): string[] {\n if (!item) return [theme.fg('dim', NO_SELECTION)];\n\n const position = currentWorkflowPosition(item.jobs);\n const elapsed = humanizeDuration(Math.max(0, Date.now() - Date.parse(item.startedAt)));\n const executionState = item.executionState ?? RUNNING_STATE;\n const stateColour: ThemeColor = executionState === PAUSED_STATE ? 'warning' : 'dim';\n const lines = [\n rightAligned(\n `${theme.fg('accent', '●')} ${theme.bold(item.displayName)}`,\n theme.fg(stateColour, executionState),\n width,\n ),\n truncateToWidth(theme.fg('dim', `${item.runKey} · ${item.workspace} · ${elapsed}`), width, ELLIPSIS),\n position\n ? truncateToWidth(\n `${theme.fg('accent', position.job)}${position.step ? theme.fg('muted', ` · ${position.step}`) : ''}`,\n width,\n ELLIPSIS,\n )\n : theme.fg('dim', 'starting'),\n '',\n theme.fg('dim', PROGRESS_HEADING),\n ];\n\n // The output tail claims its rows first: progress is a list that can be paged\n // to by opening the run, while the tail is the only live signal on screen.\n const reservedOutput = item.output.length > 0 ? MIN_OUTPUT_ROWS + 1 : 0;\n const progressRoom = Math.max(0, height - lines.length - reservedOutput);\n lines.push(...progressLines(item, width, theme).slice(0, progressRoom));\n\n if (item.output.length > 0 && height - lines.length >= MIN_OUTPUT_ROWS) {\n lines.push('', theme.fg('dim', OUTPUT_HEADING));\n const outputRoom = Math.max(0, height - lines.length);\n lines.push(...item.output.slice(-outputRoom).map((line) => truncateToWidth(line, width, ELLIPSIS)));\n }\n\n return lines.slice(0, height);\n}\n\nexport class WorkflowInspectorComponent extends DoomOverlay {\n private bodyHeight = 8;\n private completed = false;\n private error: string | undefined;\n private items: WorkflowInspectorItem[] = [];\n private refreshActive = false;\n private selected = 0;\n private selectedKey: string | undefined;\n private readonly timer: ReturnType<typeof setInterval>;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly source: WorkflowInspectorSource,\n private readonly done: (selection: WorkflowInspectorSelection | undefined) => void,\n options: WorkflowInspectorOptions = {},\n ) {\n super(tui, theme);\n this.selectedKey = options.initialKey;\n void this.refresh();\n this.timer = setInterval(() => void this.refresh(), options.refreshMs ?? DEFAULT_REFRESH_MS);\n this.timer.unref?.();\n }\n\n private async refresh(): Promise<void> {\n if (this.refreshActive || this.completed) return;\n this.refreshActive = true;\n try {\n const previousKey = this.items[this.selected]?.key ?? this.selectedKey;\n const items = await this.source.list();\n if (this.completed) return;\n const preserved = previousKey ? items.findIndex((item) => item.key === previousKey) : -1;\n this.items = items;\n this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, items.length - 1));\n this.selectedKey = items[this.selected]?.key;\n const selected = items[this.selected];\n if (selected) selected.output = await this.source.output(selected);\n this.error = undefined;\n if (!this.completed) this.tui.requestRender();\n } catch (cause) {\n this.error = cause instanceof Error ? cause.message : String(cause);\n if (!this.completed) this.tui.requestRender();\n } finally {\n this.refreshActive = false;\n }\n }\n\n private move(delta: number): void {\n if (this.items.length === 0) return;\n this.selected = Math.max(0, Math.min(this.items.length - 1, this.selected + delta));\n this.selectedKey = this.items[this.selected]?.key;\n void this.refreshSelectedOutput();\n this.tui.requestRender();\n }\n\n private async refreshSelectedOutput(): Promise<void> {\n const selected = this.items[this.selected];\n if (!selected || this.completed) return;\n try {\n selected.output = await this.source.output(selected);\n this.error = undefined;\n } catch (cause) {\n this.error = cause instanceof Error ? cause.message : String(cause);\n }\n if (!this.completed && selected.key === this.selectedKey) this.tui.requestRender();\n }\n\n private finish(selection: WorkflowInspectorSelection | undefined): void {\n if (this.completed) return;\n this.completed = true;\n clearInterval(this.timer);\n this.done(selection);\n }\n\n close(): void {\n this.finish(undefined);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.finish(undefined);\n return;\n }\n if (matchesKey(data, 'up') || data === 'k') {\n this.move(-1);\n return;\n }\n if (matchesKey(data, 'down') || data === 'j') {\n this.move(1);\n return;\n }\n if (matchesKey(data, 'pageUp')) {\n this.move(-this.rosterCapacity());\n return;\n }\n if (matchesKey(data, 'pageDown')) {\n this.move(this.rosterCapacity());\n return;\n }\n if (matchesKey(data, 'enter')) {\n const selected = this.items[this.selected];\n if (selected) {\n this.finish({\n ...(selected.runId ? { runId: selected.runId } : {}),\n runKey: selected.runKey,\n startedAt: selected.startedAt,\n workspace: selected.workspace,\n });\n }\n }\n }\n\n /** Runs the roster can draw, which is also what a page key moves by. */\n private rosterCapacity(): number {\n return Math.max(1, Math.floor((this.bodyHeight - ROSTER_CHROME_ROWS) / ROWS_PER_ITEM));\n }\n\n private current(): WorkflowInspectorItem | undefined {\n if (this.items.length === 0) return undefined;\n return this.items[Math.min(this.selected, this.items.length - 1)];\n }\n\n protected getChrome(): DoomOverlayChrome {\n const selected = this.current();\n const summary = selected\n ? `${selected.displayName} · ${selected.executionState ?? RUNNING_STATE}`\n : 'no active workflows';\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${this.items.length} running · ${summary}`,\n footer: FOOTER,\n footerHints: FOOTER_HINTS,\n footerRight:\n this.items.length > 0 ? `${Math.min(this.selected, this.items.length - 1) + 1}/${this.items.length}` : 'empty',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n this.bodyHeight = height;\n const detailFor = (paneWidth: number): string[] =>\n this.error\n ? [this.theme.fg('warning', `Unable to refresh workflows: ${this.error}`)]\n : detailLines(this.current(), paneWidth, height, this.theme);\n\n // Too narrow for two useful columns: the detail takes the whole body rather\n // than shredding both panes into stubs.\n if (width < MIN_TWO_PANE_WIDTH) return detailFor(width).slice(0, height);\n\n const leftWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * LIST_PANE_RATIO));\n const rightWidth = Math.max(1, width - leftWidth - 1);\n const leftContent = Math.max(1, leftWidth - PANE_GUTTER);\n const rightContent = Math.max(1, rightWidth - PANE_GUTTER);\n\n const left = this.rosterLines(leftContent);\n const right = detailFor(rightContent);\n const divider = this.theme.fg('borderMuted', '│');\n return Array.from({ length: height }, (_, index) => {\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fit(right[index] ?? '', rightContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n }\n\n private rosterLines(width: number): string[] {\n const lines = [this.theme.bold(ROSTER_HEADING), ''];\n if (this.items.length === 0) {\n lines.push(this.theme.fg('dim', EMPTY_ROSTER));\n return lines;\n }\n\n // Two rows per run: at a third of the width a single row left the name with\n // a handful of columns once the stage took its share.\n const budget = this.rosterCapacity();\n const active = Math.min(this.selected, this.items.length - 1);\n const start = Math.max(0, Math.min(active - budget + 1, Math.max(0, this.items.length - budget)));\n for (const [offset, item] of this.items.slice(start, start + budget).entries()) {\n const current = start + offset === active;\n const paused = item.executionState === PAUSED_STATE;\n const marker = current ? this.theme.fg('accent', SELECTION_MARKER) : ' ';\n const glyph = this.theme.fg(paused ? 'warning' : 'accent', '●');\n const name = current ? this.theme.bold(item.displayName) : item.displayName;\n const stage = paused ? PAUSED_STATE : (currentWorkflowPosition(item.jobs)?.job ?? 'starting');\n const heading = `${marker} ${glyph} ${name}`;\n const meta = `${' '.repeat(META_INDENT)}${this.theme.fg(paused ? 'warning' : 'dim', stage)}`;\n const background: ThemeBg = current ? 'selectedBg' : 'userMessageBg';\n for (const row of [heading, meta]) lines.push(this.theme.bg(background, fit(row, width)));\n }\n return lines;\n }\n\n /** Overridden: a repaint request is also the cue to re-read the registry. */\n invalidate(): void {\n void this.refresh();\n }\n\n dispose(): void {\n if (this.completed) return;\n this.completed = true;\n clearInterval(this.timer);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{DOOM_FULLSCREEN_UI_OPTIONS as e,DOOM_OVERLAY_ACCENT as t,DoomOverlay as n}from"./doomOverlay.mjs";import{humanizeDuration as r}from"./workflowStatusRow.mjs";import{matchesKey as i,truncateToWidth as a,visibleWidth as o}from"@earendil-works/pi-tui";import{currentWorkflowPosition as s}from"@agimon-ai/workflow-mcp";e.overlayOptions;const c=`running`,l=`paused`,u=[[`↑↓/jk`,`select`],[`PgUp/PgDn`,`page`],[`enter`,`open`],[`esc`,`close`]];function d(e,t){let n=a(e,Math.max(0,t),`…`).replaceAll(`\x1B[0m`,``);return n+` `.repeat(Math.max(0,t-o(n)))}function f(e,t,n){let r=o(t),i=Math.max(0,n-r-1);return d(e,i)+` `.repeat(Math.max(1,n-i-r))+d(t,r)}function p(e){return e===`completed`?`✓`:e===`failed`?`✗`:`●`}function m(e){return e===`completed`?`success`:e===`failed`?`error`:`accent`}function h(e,t,n){let r=[];for(let i of e.jobs){let e=i.total?` · ${(i.index??0)+1}/${i.total}`:``;if(r.push(a(n.fg(m(i.status),`${p(i.status)} ${i.name}`)+n.fg(`dim`,e),t,`…`)),i.status===c)for(let e of i.steps){let i=e.status===`failed`?`error`:e.status===c?`muted`:`dim`;r.push(a(n.fg(i,` ${p(e.status)} ${e.name}`),t,`…`))}}return r}function g(e,t,n,i){if(!e)return[i.fg(`dim`,`Select a workflow to inspect its jobs and output.`)];let o=s(e.jobs),u=r(Math.max(0,Date.now()-Date.parse(e.startedAt))),d=e.executionState??c,p=d===l?`warning`:`dim`,m=[f(`${i.fg(`accent`,`●`)} ${i.bold(e.displayName)}`,i.fg(p,d),t),a(i.fg(`dim`,`${e.runKey} · ${e.workspace} · ${u}`),t,`…`),o?a(`${i.fg(`accent`,o.job)}${o.step?i.fg(`muted`,` · ${o.step}`):``}`,t,`…`):i.fg(`dim`,`starting`),``,i.fg(`dim`,`PROGRESS`)],g=e.output.length>0?3:0,_=Math.max(0,n-m.length-g);if(m.push(...h(e,t,i).slice(0,_)),e.output.length>0&&n-m.length>=2){m.push(``,i.fg(`dim`,`OUTPUT · newest last`));let r=Math.max(0,n-m.length);m.push(...e.output.slice(-r).map(e=>a(e,t,`…`)))}return m.slice(0,n)}var _=class extends n{source;done;bodyHeight=8;completed=!1;error;items=[];refreshActive=!1;selected=0;selectedKey;timer;constructor(e,t,n,r,i={}){super(e,t),this.source=n,this.done=r,this.selectedKey=i.initialKey,this.refresh(),this.timer=setInterval(()=>void this.refresh(),i.refreshMs??750),this.timer.unref?.()}async refresh(){if(!(this.refreshActive||this.completed)){this.refreshActive=!0;try{let e=this.items[this.selected]?.key??this.selectedKey,t=await this.source.list();if(this.completed)return;let n=e?t.findIndex(t=>t.key===e):-1;this.items=t,this.selected=n>=0?n:Math.min(this.selected,Math.max(0,t.length-1)),this.selectedKey=t[this.selected]?.key;let r=t[this.selected];r&&(r.output=await this.source.output(r)),this.error=void 0,this.completed||this.tui.requestRender()}catch(e){this.error=e instanceof Error?e.message:String(e),this.completed||this.tui.requestRender()}finally{this.refreshActive=!1}}}move(e){this.items.length!==0&&(this.selected=Math.max(0,Math.min(this.items.length-1,this.selected+e)),this.selectedKey=this.items[this.selected]?.key,this.refreshSelectedOutput(),this.tui.requestRender())}async refreshSelectedOutput(){let e=this.items[this.selected];if(!(!e||this.completed)){try{e.output=await this.source.output(e),this.error=void 0}catch(e){this.error=e instanceof Error?e.message:String(e)}!this.completed&&e.key===this.selectedKey&&this.tui.requestRender()}}finish(e){this.completed||(this.completed=!0,clearInterval(this.timer),this.done(e))}close(){this.finish(void 0)}handleInput(e){if(i(e,`escape`)||i(e,`ctrl+c`)){this.finish(void 0);return}if(i(e,`up`)||e===`k`){this.move(-1);return}if(i(e,`down`)||e===`j`){this.move(1);return}if(i(e,`pageUp`)){this.move(-this.rosterCapacity());return}if(i(e,`pageDown`)){this.move(this.rosterCapacity());return}if(i(e,`enter`)){let e=this.items[this.selected];e&&this.finish({...e.runId?{runId:e.runId}:{},runKey:e.runKey,startedAt:e.startedAt,workspace:e.workspace})}}rosterCapacity(){return Math.max(1,Math.floor((this.bodyHeight-2)/2))}current(){if(this.items.length!==0)return this.items[Math.min(this.selected,this.items.length-1)]}getChrome(){let e=this.current(),n=e?`${e.displayName} · ${e.executionState??c}`:`no active workflows`;return{title:`WORKFLOW SPACE`,accent:t,breadcrumb:`SPC › w / workflows › l / manage`,headerRight:`${this.items.length} running · ${n}`,footer:`↑↓/jk select · PgUp/PgDn page · enter open · esc close`,footerHints:u,footerRight:this.items.length>0?`${Math.min(this.selected,this.items.length-1)+1}/${this.items.length}`:`empty`}}renderBody(e,t){this.bodyHeight=t;let n=e=>this.error?[this.theme.fg(`warning`,`Unable to refresh workflows: ${this.error}`)]:g(this.current(),e,t,this.theme);if(e<48)return n(e).slice(0,t);let r=Math.max(18,Math.floor((e-1)*.3333333333333333)),i=Math.max(1,e-r-1),o=Math.max(1,r-1),s=Math.max(1,i-1),c=this.rosterLines(o),l=n(s),u=this.theme.fg(`borderMuted`,`│`);return Array.from({length:t},(t,n)=>{let r=`${d(c[n]??``,o)} ${u} ${d(l[n]??``,s)}`;return a(r,e,`…`)})}rosterLines(e){let t=[this.theme.bold(`RUNNING · this session`),``];if(this.items.length===0)return t.push(this.theme.fg(`dim`,`No active workflows`)),t;let n=this.rosterCapacity(),r=Math.min(this.selected,this.items.length-1),i=Math.max(0,Math.min(r-n+1,Math.max(0,this.items.length-n)));for(let[a,o]of this.items.slice(i,i+n).entries()){let n=i+a===r,c=o.executionState===l,u=n?this.theme.fg(`accent`,`›`):` `,f=this.theme.fg(c?`warning`:`accent`,`●`),p=n?this.theme.bold(o.displayName):o.displayName,m=c?l:s(o.jobs)?.job??`starting`,h=`${u} ${f} ${p}`,g=`${` `.repeat(4)}${this.theme.fg(c?`warning`:`dim`,m)}`,_=n?`selectedBg`:`userMessageBg`;for(let n of[h,g])t.push(this.theme.bg(_,d(n,e)))}return t}invalidate(){this.refresh()}dispose(){this.completed||(this.completed=!0,clearInterval(this.timer))}};export{_ as WorkflowInspectorComponent};
2
+ //# sourceMappingURL=workflowInspector.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowInspector.mjs","names":[],"sources":["../../src/workflow/workflowInspector.ts"],"sourcesContent":["/**\n * Workflow Space: the `SPC w l` overlay.\n *\n * A doom overlay rather than a hand-rolled frame: the roster owns the left\n * third, the run under the cursor owns the right two thirds, and the chrome is\n * the same frame, breadcrumb and key legend every other doom surface draws.\n *\n * Rendering is pure -- `renderBody(width, height)` reads the last polled\n * snapshot and returns lines with no side effects -- so the surface can be\n * asserted as text without a live terminal.\n */\n\nimport type { Theme } from '@earendil-works/pi-coding-agent';\nimport {\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n DOOM_FULLSCREEN_UI_OPTIONS,\n} from './doomOverlay.ts';\nimport { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport { currentWorkflowPosition, type WorkflowProgressJob } from '@agimon-ai/workflow-mcp';\nimport { humanizeDuration } from './workflowStatusRow';\n\nexport const WORKFLOW_INSPECTOR_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nexport interface WorkflowInspectorSelection {\n runId?: string;\n runKey: string;\n startedAt: string;\n workspace: string;\n}\n\nexport interface WorkflowInspectorItem extends WorkflowInspectorSelection {\n displayName: string;\n executionState?: string;\n jobs: WorkflowProgressJob[];\n key: string;\n output: string[];\n}\n\nexport interface WorkflowInspectorSource {\n list(): Promise<WorkflowInspectorItem[]>;\n output(item: WorkflowInspectorItem): Promise<string[]>;\n}\n\nexport interface WorkflowInspectorOptions {\n initialKey?: string;\n refreshMs?: number;\n}\n\nconst TITLE = 'WORKFLOW SPACE';\nconst BREADCRUMB = 'SPC › w / workflows › l / manage';\nconst ROSTER_HEADING = 'RUNNING · this session';\nconst EMPTY_ROSTER = 'No active workflows';\nconst PROGRESS_HEADING = 'PROGRESS';\nconst OUTPUT_HEADING = 'OUTPUT · newest last';\nconst NO_SELECTION = 'Select a workflow to inspect its jobs and output.';\nconst RUNNING_STATE = 'running';\nconst PAUSED_STATE = 'paused';\nconst FOOTER = '↑↓/jk select · PgUp/PgDn page · enter open · esc close';\nconst FOOTER_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓/jk', 'select'],\n ['PgUp/PgDn', 'page'],\n ['enter', 'open'],\n ['esc', 'close'],\n];\n\nconst DEFAULT_REFRESH_MS = 750;\nconst SELECTION_MARKER = '›';\nconst ELLIPSIS = '…';\n/** pi-tui brackets a truncation ellipsis with this; see `fit`. */\nconst HARD_RESET = '\\x1b[0m';\n/** A third for the roster, two thirds for the run being read. */\nconst LIST_PANE_RATIO = 1 / 3;\n/** One blank column each side of the divider so neither pane touches it. */\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 18;\n/** Below this the roster cannot hold a readable name, so the detail takes the body. */\nconst MIN_TWO_PANE_WIDTH = 48;\n/** Each run occupies a name row and a state row beneath it. */\nconst ROWS_PER_ITEM = 2;\n/** Hanging indent for the state row: clears the marker and the glyph. */\nconst META_INDENT = 4;\n/** Rows the roster heading and its spacer take. */\nconst ROSTER_CHROME_ROWS = 2;\n/** The output tail keeps at least this many rows, or it is not worth a divider. */\nconst MIN_OUTPUT_ROWS = 2;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\n/**\n * Truncates and pads to an exact column count.\n *\n * `truncateToWidth` wraps its ellipsis in a hard `\\x1b[0m`, which would strip a\n * row's background for everything after a clipped name. The theme closes its own\n * colours with `\\x1b[39m` and `\\x1b[49m`, so the injected resets are dropped.\n */\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS).replaceAll(HARD_RESET, '');\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction rightAligned(left: string, right: string, width: number): string {\n const rightWidth = visibleWidth(right);\n const leftWidth = Math.max(0, width - rightWidth - 1);\n return fit(left, leftWidth) + ' '.repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);\n}\n\nfunction jobGlyph(status: string): string {\n return status === 'completed' ? '✓' : status === 'failed' ? '✗' : '●';\n}\n\nfunction jobColour(status: string): ThemeColor {\n return status === 'completed' ? 'success' : status === 'failed' ? 'error' : 'accent';\n}\n\nfunction progressLines(item: WorkflowInspectorItem, width: number, theme: Theme): string[] {\n const lines: string[] = [];\n for (const job of item.jobs) {\n const position = job.total ? ` · ${(job.index ?? 0) + 1}/${job.total}` : '';\n lines.push(\n truncateToWidth(\n theme.fg(jobColour(job.status), `${jobGlyph(job.status)} ${job.name}`) + theme.fg('dim', position),\n width,\n ELLIPSIS,\n ),\n );\n // Only the running job's steps: every finished job's steps would push the\n // live one off the pane, which is the one thing worth watching.\n if (job.status !== RUNNING_STATE) continue;\n for (const step of job.steps) {\n const stepColour: ThemeColor =\n step.status === 'failed' ? 'error' : step.status === RUNNING_STATE ? 'muted' : 'dim';\n lines.push(truncateToWidth(theme.fg(stepColour, ` ${jobGlyph(step.status)} ${step.name}`), width, ELLIPSIS));\n }\n }\n return lines;\n}\n\nfunction detailLines(item: WorkflowInspectorItem | undefined, width: number, height: number, theme: Theme): string[] {\n if (!item) return [theme.fg('dim', NO_SELECTION)];\n\n const position = currentWorkflowPosition(item.jobs);\n const elapsed = humanizeDuration(Math.max(0, Date.now() - Date.parse(item.startedAt)));\n const executionState = item.executionState ?? RUNNING_STATE;\n const stateColour: ThemeColor = executionState === PAUSED_STATE ? 'warning' : 'dim';\n const lines = [\n rightAligned(\n `${theme.fg('accent', '●')} ${theme.bold(item.displayName)}`,\n theme.fg(stateColour, executionState),\n width,\n ),\n truncateToWidth(theme.fg('dim', `${item.runKey} · ${item.workspace} · ${elapsed}`), width, ELLIPSIS),\n position\n ? truncateToWidth(\n `${theme.fg('accent', position.job)}${position.step ? theme.fg('muted', ` · ${position.step}`) : ''}`,\n width,\n ELLIPSIS,\n )\n : theme.fg('dim', 'starting'),\n '',\n theme.fg('dim', PROGRESS_HEADING),\n ];\n\n // The output tail claims its rows first: progress is a list that can be paged\n // to by opening the run, while the tail is the only live signal on screen.\n const reservedOutput = item.output.length > 0 ? MIN_OUTPUT_ROWS + 1 : 0;\n const progressRoom = Math.max(0, height - lines.length - reservedOutput);\n lines.push(...progressLines(item, width, theme).slice(0, progressRoom));\n\n if (item.output.length > 0 && height - lines.length >= MIN_OUTPUT_ROWS) {\n lines.push('', theme.fg('dim', OUTPUT_HEADING));\n const outputRoom = Math.max(0, height - lines.length);\n lines.push(...item.output.slice(-outputRoom).map((line) => truncateToWidth(line, width, ELLIPSIS)));\n }\n\n return lines.slice(0, height);\n}\n\nexport class WorkflowInspectorComponent extends DoomOverlay {\n private bodyHeight = 8;\n private completed = false;\n private error: string | undefined;\n private items: WorkflowInspectorItem[] = [];\n private refreshActive = false;\n private selected = 0;\n private selectedKey: string | undefined;\n private readonly timer: ReturnType<typeof setInterval>;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly source: WorkflowInspectorSource,\n private readonly done: (selection: WorkflowInspectorSelection | undefined) => void,\n options: WorkflowInspectorOptions = {},\n ) {\n super(tui, theme);\n this.selectedKey = options.initialKey;\n void this.refresh();\n this.timer = setInterval(() => void this.refresh(), options.refreshMs ?? DEFAULT_REFRESH_MS);\n this.timer.unref?.();\n }\n\n private async refresh(): Promise<void> {\n if (this.refreshActive || this.completed) return;\n this.refreshActive = true;\n try {\n const previousKey = this.items[this.selected]?.key ?? this.selectedKey;\n const items = await this.source.list();\n if (this.completed) return;\n const preserved = previousKey ? items.findIndex((item) => item.key === previousKey) : -1;\n this.items = items;\n this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, items.length - 1));\n this.selectedKey = items[this.selected]?.key;\n const selected = items[this.selected];\n if (selected) selected.output = await this.source.output(selected);\n this.error = undefined;\n if (!this.completed) this.tui.requestRender();\n } catch (cause) {\n this.error = cause instanceof Error ? cause.message : String(cause);\n if (!this.completed) this.tui.requestRender();\n } finally {\n this.refreshActive = false;\n }\n }\n\n private move(delta: number): void {\n if (this.items.length === 0) return;\n this.selected = Math.max(0, Math.min(this.items.length - 1, this.selected + delta));\n this.selectedKey = this.items[this.selected]?.key;\n void this.refreshSelectedOutput();\n this.tui.requestRender();\n }\n\n private async refreshSelectedOutput(): Promise<void> {\n const selected = this.items[this.selected];\n if (!selected || this.completed) return;\n try {\n selected.output = await this.source.output(selected);\n this.error = undefined;\n } catch (cause) {\n this.error = cause instanceof Error ? cause.message : String(cause);\n }\n if (!this.completed && selected.key === this.selectedKey) this.tui.requestRender();\n }\n\n private finish(selection: WorkflowInspectorSelection | undefined): void {\n if (this.completed) return;\n this.completed = true;\n clearInterval(this.timer);\n this.done(selection);\n }\n\n close(): void {\n this.finish(undefined);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.finish(undefined);\n return;\n }\n if (matchesKey(data, 'up') || data === 'k') {\n this.move(-1);\n return;\n }\n if (matchesKey(data, 'down') || data === 'j') {\n this.move(1);\n return;\n }\n if (matchesKey(data, 'pageUp')) {\n this.move(-this.rosterCapacity());\n return;\n }\n if (matchesKey(data, 'pageDown')) {\n this.move(this.rosterCapacity());\n return;\n }\n if (matchesKey(data, 'enter')) {\n const selected = this.items[this.selected];\n if (selected) {\n this.finish({\n ...(selected.runId ? { runId: selected.runId } : {}),\n runKey: selected.runKey,\n startedAt: selected.startedAt,\n workspace: selected.workspace,\n });\n }\n }\n }\n\n /** Runs the roster can draw, which is also what a page key moves by. */\n private rosterCapacity(): number {\n return Math.max(1, Math.floor((this.bodyHeight - ROSTER_CHROME_ROWS) / ROWS_PER_ITEM));\n }\n\n private current(): WorkflowInspectorItem | undefined {\n if (this.items.length === 0) return undefined;\n return this.items[Math.min(this.selected, this.items.length - 1)];\n }\n\n protected getChrome(): DoomOverlayChrome {\n const selected = this.current();\n const summary = selected\n ? `${selected.displayName} · ${selected.executionState ?? RUNNING_STATE}`\n : 'no active workflows';\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${this.items.length} running · ${summary}`,\n footer: FOOTER,\n footerHints: FOOTER_HINTS,\n footerRight:\n this.items.length > 0 ? `${Math.min(this.selected, this.items.length - 1) + 1}/${this.items.length}` : 'empty',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n this.bodyHeight = height;\n const detailFor = (paneWidth: number): string[] =>\n this.error\n ? [this.theme.fg('warning', `Unable to refresh workflows: ${this.error}`)]\n : detailLines(this.current(), paneWidth, height, this.theme);\n\n // Too narrow for two useful columns: the detail takes the whole body rather\n // than shredding both panes into stubs.\n if (width < MIN_TWO_PANE_WIDTH) return detailFor(width).slice(0, height);\n\n const leftWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * LIST_PANE_RATIO));\n const rightWidth = Math.max(1, width - leftWidth - 1);\n const leftContent = Math.max(1, leftWidth - PANE_GUTTER);\n const rightContent = Math.max(1, rightWidth - PANE_GUTTER);\n\n const left = this.rosterLines(leftContent);\n const right = detailFor(rightContent);\n const divider = this.theme.fg('borderMuted', '│');\n return Array.from({ length: height }, (_, index) => {\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fit(right[index] ?? '', rightContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n }\n\n private rosterLines(width: number): string[] {\n const lines = [this.theme.bold(ROSTER_HEADING), ''];\n if (this.items.length === 0) {\n lines.push(this.theme.fg('dim', EMPTY_ROSTER));\n return lines;\n }\n\n // Two rows per run: at a third of the width a single row left the name with\n // a handful of columns once the stage took its share.\n const budget = this.rosterCapacity();\n const active = Math.min(this.selected, this.items.length - 1);\n const start = Math.max(0, Math.min(active - budget + 1, Math.max(0, this.items.length - budget)));\n for (const [offset, item] of this.items.slice(start, start + budget).entries()) {\n const current = start + offset === active;\n const paused = item.executionState === PAUSED_STATE;\n const marker = current ? this.theme.fg('accent', SELECTION_MARKER) : ' ';\n const glyph = this.theme.fg(paused ? 'warning' : 'accent', '●');\n const name = current ? this.theme.bold(item.displayName) : item.displayName;\n const stage = paused ? PAUSED_STATE : (currentWorkflowPosition(item.jobs)?.job ?? 'starting');\n const heading = `${marker} ${glyph} ${name}`;\n const meta = `${' '.repeat(META_INDENT)}${this.theme.fg(paused ? 'warning' : 'dim', stage)}`;\n const background: ThemeBg = current ? 'selectedBg' : 'userMessageBg';\n for (const row of [heading, meta]) lines.push(this.theme.bg(background, fit(row, width)));\n }\n return lines;\n }\n\n /** Overridden: a repaint request is also the cue to re-read the registry. */\n invalidate(): void {\n void this.refresh();\n }\n\n dispose(): void {\n if (this.completed) return;\n this.completed = true;\n clearInterval(this.timer);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require("./piToolBridge.cjs");let t=require("node:path"),n=require("zod");const r=`user_prompt`,i=n.z.object({description:n.z.string().optional().describe(`Description shown for the workflow input.`),required:n.z.boolean().optional().describe(`Whether the workflow input must be provided.`),default:n.z.string().optional().describe(`Default value for the workflow input.`),type:n.z.string().optional().describe(`Declared workflow input type.`),options:n.z.array(n.z.string()).optional().describe(`Allowed values for the workflow input.`)}).passthrough(),a=n.z.object({directory:n.z.string().min(1).describe(`Directory searched for workflow definitions.`),hasNextPage:n.z.boolean().describe(`Whether another catalog page exists.`),page:n.z.number().int().positive().describe(`One-based catalog page number.`),pageSize:n.z.number().int().positive().describe(`Catalog page size.`),workflows:n.z.array(n.z.object({path:n.z.string().min(1).describe(`Workflow path relative to the catalog directory.`),name:n.z.string().describe(`Workflow display name.`),description:n.z.string().describe(`Workflow description.`),tags:n.z.array(n.z.string()).describe(`Workflow tags.`)}).passthrough()).describe(`Workflow definitions on this page.`)}).passthrough();function o(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function s(e){return e&&typeof e==`object`&&!Array.isArray(e)?{...e}:{}}function c(e){return s(e.on)}function l(e){let t=c(e).workflow_dispatch;if(t==null)return{};let r=n.z.object({inputs:n.z.record(n.z.string(),i).optional().describe(`Workflow dispatch input definitions.`)}).passthrough().safeParse(t);if(!r.success)throw Error(`Invalid workflow_dispatch input definitions: ${r.error.message}`);return r.data.inputs??{}}function u(t){return Error(e.toolResultText(t)||`Workflow launch failed.`)}function d(n){if(n.isError)throw u(n);let r;try{r=JSON.parse(e.toolResultText(n))}catch(e){throw Error(`Workflow catalog returned invalid JSON.`,{cause:e})}let i=a.safeParse(r);if(!i.success)throw Error(`Workflow catalog response was invalid: ${i.error.message}`);return{directory:i.data.directory,hasNextPage:i.data.hasNextPage,page:i.data.page,pageSize:i.data.pageSize,workflows:i.data.workflows.map(e=>({description:e.description,name:e.name,path:(0,t.resolve)(i.data.directory,e.path),relativePath:e.path,tags:e.tags}))}}async function f(e,t){let n=[],r=1,i=!0;for(;i;){let a={directory:t,page:r,pageSize:100},o=d(await e.execute(a));n.push(...o.workflows),i=o.hasNextPage,r+=1}return n}async function p(e){for(;;){let t=await e.editor(`Workflow prompt`);if(t===void 0)return;let n=t.trim();if(n)return n;e.notify(`A workflow prompt is required.`,`warning`)}}async function m(e,t,n){let r=n.description?`${t}: ${n.description}`:`Workflow input: ${t}`;if(n.options&&n.options.length>0)return e.select(r,n.options);if(n.type===`boolean`)return e.select(r,[`true`,`false`]);for(;;){let i=await e.input(r,n.default);if(i===void 0)return;let a=i.trim()||n.default;if(a||!n.required)return a||void 0;e.notify(`A value is required for workflow input ${t}.`,`warning`)}}async function h(e,t){let n=c(e),i=o(n,r)?await p(t):void 0;if(o(n,r)&&i===void 0)return;let a=l(e),s={};for(let[e,n]of Object.entries(a)){let r=await m(t,e,n);if(r===void 0&&n.required)return;r!==void 0&&(s[e]=r)}return{...i===void 0?{}:{prompt:i},...Object.keys(s).length===0?{}:{inputs:s}}}async function g(e,t){let n=await f(e.listWorkflowsTool,e.cwd);if(n.length===0){e.ui.notify(`No workflow definitions were found in this repository.`,`warning`);return}let r=await e.pickWorkflow(n);if(r===void 0)return;let i=e.parseWorkflow(r.path),a=e.compatibleRunners(i);if(a&&a.length===0)throw Error(`No compatible runner is available for this workflow.`);let o;if(a&&(o=await e.ui.select(`Select a workflow runner`,a),o===void 0))return;let s=await h(i,e.ui);if(s===void 0)return;let c={workflowPath:r.path,...o===void 0?{}:{runner:o},...typeof i.workspace==`string`?{workspace:i.workspace}:{},...s};return e.launch(c,t)}exports.collectWorkflowInputs=h,exports.loadWorkflowCatalog=f,exports.openWorkflowLaunchWizard=g,exports.parseWorkflowCatalogPage=d;
2
+ //# sourceMappingURL=workflowLauncher.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowLauncher.cjs","names":["z","toolResultText","resolve"],"sources":["../../src/workflow/workflowLauncher.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport type { ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { type EmbeddedWorkflowFeature, type Workflow } from '@agimon-ai/workflow-mcp';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\nimport { type WorkflowLaunchExecutor, type WorkflowLaunchInput } from './piTools';\nimport { toolResultText } from './piToolBridge';\n\nconst CATALOG_PAGE_SIZE = 100;\nconst USER_PROMPT_TRIGGER = 'user_prompt';\nconst WORKFLOW_DISPATCH_TRIGGER = 'workflow_dispatch';\nconst BOOLEAN_INPUT_TYPE = 'boolean';\n\nconst dispatchInputSchema = z\n .object({\n description: z.string().optional().describe('Description shown for the workflow input.'),\n required: z.boolean().optional().describe('Whether the workflow input must be provided.'),\n default: z.string().optional().describe('Default value for the workflow input.'),\n type: z.string().optional().describe('Declared workflow input type.'),\n options: z.array(z.string()).optional().describe('Allowed values for the workflow input.'),\n })\n .passthrough();\n\nconst workflowCatalogPageSchema = z\n .object({\n directory: z.string().min(1).describe('Directory searched for workflow definitions.'),\n hasNextPage: z.boolean().describe('Whether another catalog page exists.'),\n page: z.number().int().positive().describe('One-based catalog page number.'),\n pageSize: z.number().int().positive().describe('Catalog page size.'),\n workflows: z\n .array(\n z\n .object({\n path: z.string().min(1).describe('Workflow path relative to the catalog directory.'),\n name: z.string().describe('Workflow display name.'),\n description: z.string().describe('Workflow description.'),\n tags: z.array(z.string()).describe('Workflow tags.'),\n })\n .passthrough(),\n )\n .describe('Workflow definitions on this page.'),\n })\n .passthrough();\n\ntype WorkflowListTool = EmbeddedWorkflowFeature['listWorkflowsTool'];\ntype WorkflowListInput = Parameters<WorkflowListTool['execute']>[0];\ntype DispatchInput = z.infer<typeof dispatchInputSchema>;\n\nexport interface WorkflowCatalogEntry {\n description: string;\n name: string;\n path: string;\n relativePath: string;\n tags: string[];\n}\n\nexport interface WorkflowCatalogPage {\n directory: string;\n hasNextPage: boolean;\n page: number;\n pageSize: number;\n workflows: WorkflowCatalogEntry[];\n}\n\nexport interface WorkflowLauncherUi {\n editor(title: string, prefill?: string): Promise<string | undefined>;\n input(title: string, placeholder?: string): Promise<string | undefined>;\n notify(message: string, type?: 'info' | 'warning' | 'error'): void;\n select(title: string, options: string[]): Promise<string | undefined>;\n}\n\nexport interface WorkflowLaunchWizardOptions {\n compatibleRunners(workflow: Workflow): string[] | undefined;\n cwd: string;\n launch: WorkflowLaunchExecutor['execute'];\n listWorkflowsTool: WorkflowListTool;\n parseWorkflow(workflowPath: string): Workflow;\n /**\n * Workflow selection, injected rather than taken from `ui.select`: a catalog\n * of this size needs a scrollable, filterable surface, and keeping it out of\n * here leaves the wizard assertable without a terminal.\n */\n pickWorkflow(entries: readonly WorkflowCatalogEntry[]): Promise<WorkflowCatalogEntry | undefined>;\n ui: WorkflowLauncherUi;\n}\n\nfunction hasOwnProperty(value: Record<string, unknown>, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(value, key);\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {};\n}\n\nfunction workflowTriggerBlock(workflow: Workflow): Record<string, unknown> {\n return asRecord(workflow.on);\n}\n\nfunction dispatchInputs(workflow: Workflow): Record<string, DispatchInput> {\n const dispatch = workflowTriggerBlock(workflow)[WORKFLOW_DISPATCH_TRIGGER];\n if (dispatch === null || dispatch === undefined) return {};\n const parsed = z\n .object({\n inputs: z.record(z.string(), dispatchInputSchema).optional().describe('Workflow dispatch input definitions.'),\n })\n .passthrough()\n .safeParse(dispatch);\n if (!parsed.success) throw new Error(`Invalid workflow_dispatch input definitions: ${parsed.error.message}`);\n return parsed.data.inputs ?? {};\n}\n\nfunction launchResultError(result: CallToolResult): Error {\n return new Error(toolResultText(result) || 'Workflow launch failed.');\n}\n\nexport function parseWorkflowCatalogPage(result: CallToolResult): WorkflowCatalogPage {\n if (result.isError) throw launchResultError(result);\n let value: unknown;\n try {\n value = JSON.parse(toolResultText(result));\n } catch (cause) {\n throw new Error('Workflow catalog returned invalid JSON.', { cause });\n }\n const parsed = workflowCatalogPageSchema.safeParse(value);\n if (!parsed.success) throw new Error(`Workflow catalog response was invalid: ${parsed.error.message}`);\n return {\n directory: parsed.data.directory,\n hasNextPage: parsed.data.hasNextPage,\n page: parsed.data.page,\n pageSize: parsed.data.pageSize,\n workflows: parsed.data.workflows.map((entry) => ({\n description: entry.description,\n name: entry.name,\n path: resolve(parsed.data.directory, entry.path),\n relativePath: entry.path,\n tags: entry.tags,\n })),\n };\n}\n\nexport async function loadWorkflowCatalog(\n listWorkflowsTool: WorkflowListTool,\n directory: string,\n): Promise<WorkflowCatalogEntry[]> {\n const entries: WorkflowCatalogEntry[] = [];\n let page = 1;\n let hasNextPage = true;\n while (hasNextPage) {\n const input: WorkflowListInput = { directory, page, pageSize: CATALOG_PAGE_SIZE };\n const result = await listWorkflowsTool.execute(input);\n const catalogPage = parseWorkflowCatalogPage(result);\n entries.push(...catalogPage.workflows);\n hasNextPage = catalogPage.hasNextPage;\n page += 1;\n }\n return entries;\n}\n\nasync function requestPrompt(ui: WorkflowLauncherUi): Promise<string | undefined> {\n while (true) {\n const value = await ui.editor('Workflow prompt');\n if (value === undefined) return undefined;\n const prompt = value.trim();\n if (prompt) return prompt;\n ui.notify('A workflow prompt is required.', 'warning');\n }\n}\n\nasync function requestDispatchInput(\n ui: WorkflowLauncherUi,\n name: string,\n input: DispatchInput,\n): Promise<string | undefined> {\n const title = input.description ? `${name}: ${input.description}` : `Workflow input: ${name}`;\n if (input.options && input.options.length > 0) return ui.select(title, input.options);\n if (input.type === BOOLEAN_INPUT_TYPE) return ui.select(title, ['true', 'false']);\n\n while (true) {\n const value = await ui.input(title, input.default);\n if (value === undefined) return undefined;\n const normalized = value.trim() || input.default;\n if (normalized || !input.required) return normalized || undefined;\n ui.notify(`A value is required for workflow input ${name}.`, 'warning');\n }\n}\n\nexport async function collectWorkflowInputs(\n workflow: Workflow,\n ui: WorkflowLauncherUi,\n): Promise<{ inputs?: Record<string, string>; prompt?: string } | undefined> {\n const triggers = workflowTriggerBlock(workflow);\n const prompt = hasOwnProperty(triggers, USER_PROMPT_TRIGGER) ? await requestPrompt(ui) : undefined;\n if (hasOwnProperty(triggers, USER_PROMPT_TRIGGER) && prompt === undefined) return undefined;\n\n const definitions = dispatchInputs(workflow);\n const inputs: Record<string, string> = {};\n for (const [name, definition] of Object.entries(definitions)) {\n const value = await requestDispatchInput(ui, name, definition);\n if (value === undefined && definition.required) return undefined;\n if (value !== undefined) inputs[name] = value;\n }\n return {\n ...(prompt === undefined ? {} : { prompt }),\n ...(Object.keys(inputs).length === 0 ? {} : { inputs }),\n };\n}\n\nexport async function openWorkflowLaunchWizard(\n options: WorkflowLaunchWizardOptions,\n ctx: ExtensionContext,\n): Promise<CallToolResult | undefined> {\n const entries = await loadWorkflowCatalog(options.listWorkflowsTool, options.cwd);\n if (entries.length === 0) {\n options.ui.notify('No workflow definitions were found in this repository.', 'warning');\n return undefined;\n }\n\n const selected = await options.pickWorkflow(entries);\n if (selected === undefined) return undefined;\n\n const workflow = options.parseWorkflow(selected.path);\n const runners = options.compatibleRunners(workflow);\n if (runners && runners.length === 0) throw new Error('No compatible runner is available for this workflow.');\n let runner: string | undefined;\n if (runners) {\n runner = await options.ui.select('Select a workflow runner', runners);\n if (runner === undefined) return undefined;\n }\n\n const triggerInputs = await collectWorkflowInputs(workflow, options.ui);\n if (triggerInputs === undefined) return undefined;\n const launchInput: WorkflowLaunchInput = {\n workflowPath: selected.path,\n ...(runner === undefined ? {} : { runner }),\n ...(typeof workflow.workspace === 'string' ? { workspace: workflow.workspace } : {}),\n ...triggerInputs,\n };\n return options.launch(launchInput, ctx);\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{toolResultText as e}from"./piToolBridge.mjs";import{resolve as t}from"node:path";import{z as n}from"zod";const r=`user_prompt`,i=n.object({description:n.string().optional().describe(`Description shown for the workflow input.`),required:n.boolean().optional().describe(`Whether the workflow input must be provided.`),default:n.string().optional().describe(`Default value for the workflow input.`),type:n.string().optional().describe(`Declared workflow input type.`),options:n.array(n.string()).optional().describe(`Allowed values for the workflow input.`)}).passthrough(),a=n.object({directory:n.string().min(1).describe(`Directory searched for workflow definitions.`),hasNextPage:n.boolean().describe(`Whether another catalog page exists.`),page:n.number().int().positive().describe(`One-based catalog page number.`),pageSize:n.number().int().positive().describe(`Catalog page size.`),workflows:n.array(n.object({path:n.string().min(1).describe(`Workflow path relative to the catalog directory.`),name:n.string().describe(`Workflow display name.`),description:n.string().describe(`Workflow description.`),tags:n.array(n.string()).describe(`Workflow tags.`)}).passthrough()).describe(`Workflow definitions on this page.`)}).passthrough();function o(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function s(e){return e&&typeof e==`object`&&!Array.isArray(e)?{...e}:{}}function c(e){return s(e.on)}function l(e){let t=c(e).workflow_dispatch;if(t==null)return{};let r=n.object({inputs:n.record(n.string(),i).optional().describe(`Workflow dispatch input definitions.`)}).passthrough().safeParse(t);if(!r.success)throw Error(`Invalid workflow_dispatch input definitions: ${r.error.message}`);return r.data.inputs??{}}function u(t){return Error(e(t)||`Workflow launch failed.`)}function d(n){if(n.isError)throw u(n);let r;try{r=JSON.parse(e(n))}catch(e){throw Error(`Workflow catalog returned invalid JSON.`,{cause:e})}let i=a.safeParse(r);if(!i.success)throw Error(`Workflow catalog response was invalid: ${i.error.message}`);return{directory:i.data.directory,hasNextPage:i.data.hasNextPage,page:i.data.page,pageSize:i.data.pageSize,workflows:i.data.workflows.map(e=>({description:e.description,name:e.name,path:t(i.data.directory,e.path),relativePath:e.path,tags:e.tags}))}}async function f(e,t){let n=[],r=1,i=!0;for(;i;){let a={directory:t,page:r,pageSize:100},o=d(await e.execute(a));n.push(...o.workflows),i=o.hasNextPage,r+=1}return n}async function p(e){for(;;){let t=await e.editor(`Workflow prompt`);if(t===void 0)return;let n=t.trim();if(n)return n;e.notify(`A workflow prompt is required.`,`warning`)}}async function m(e,t,n){let r=n.description?`${t}: ${n.description}`:`Workflow input: ${t}`;if(n.options&&n.options.length>0)return e.select(r,n.options);if(n.type===`boolean`)return e.select(r,[`true`,`false`]);for(;;){let i=await e.input(r,n.default);if(i===void 0)return;let a=i.trim()||n.default;if(a||!n.required)return a||void 0;e.notify(`A value is required for workflow input ${t}.`,`warning`)}}async function h(e,t){let n=c(e),i=o(n,r)?await p(t):void 0;if(o(n,r)&&i===void 0)return;let a=l(e),s={};for(let[e,n]of Object.entries(a)){let r=await m(t,e,n);if(r===void 0&&n.required)return;r!==void 0&&(s[e]=r)}return{...i===void 0?{}:{prompt:i},...Object.keys(s).length===0?{}:{inputs:s}}}async function g(e,t){let n=await f(e.listWorkflowsTool,e.cwd);if(n.length===0){e.ui.notify(`No workflow definitions were found in this repository.`,`warning`);return}let r=await e.pickWorkflow(n);if(r===void 0)return;let i=e.parseWorkflow(r.path),a=e.compatibleRunners(i);if(a&&a.length===0)throw Error(`No compatible runner is available for this workflow.`);let o;if(a&&(o=await e.ui.select(`Select a workflow runner`,a),o===void 0))return;let s=await h(i,e.ui);if(s===void 0)return;let c={workflowPath:r.path,...o===void 0?{}:{runner:o},...typeof i.workspace==`string`?{workspace:i.workspace}:{},...s};return e.launch(c,t)}export{h as collectWorkflowInputs,f as loadWorkflowCatalog,g as openWorkflowLaunchWizard,d as parseWorkflowCatalogPage};
2
+ //# sourceMappingURL=workflowLauncher.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowLauncher.mjs","names":[],"sources":["../../src/workflow/workflowLauncher.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport type { ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { type EmbeddedWorkflowFeature, type Workflow } from '@agimon-ai/workflow-mcp';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\nimport { type WorkflowLaunchExecutor, type WorkflowLaunchInput } from './piTools';\nimport { toolResultText } from './piToolBridge';\n\nconst CATALOG_PAGE_SIZE = 100;\nconst USER_PROMPT_TRIGGER = 'user_prompt';\nconst WORKFLOW_DISPATCH_TRIGGER = 'workflow_dispatch';\nconst BOOLEAN_INPUT_TYPE = 'boolean';\n\nconst dispatchInputSchema = z\n .object({\n description: z.string().optional().describe('Description shown for the workflow input.'),\n required: z.boolean().optional().describe('Whether the workflow input must be provided.'),\n default: z.string().optional().describe('Default value for the workflow input.'),\n type: z.string().optional().describe('Declared workflow input type.'),\n options: z.array(z.string()).optional().describe('Allowed values for the workflow input.'),\n })\n .passthrough();\n\nconst workflowCatalogPageSchema = z\n .object({\n directory: z.string().min(1).describe('Directory searched for workflow definitions.'),\n hasNextPage: z.boolean().describe('Whether another catalog page exists.'),\n page: z.number().int().positive().describe('One-based catalog page number.'),\n pageSize: z.number().int().positive().describe('Catalog page size.'),\n workflows: z\n .array(\n z\n .object({\n path: z.string().min(1).describe('Workflow path relative to the catalog directory.'),\n name: z.string().describe('Workflow display name.'),\n description: z.string().describe('Workflow description.'),\n tags: z.array(z.string()).describe('Workflow tags.'),\n })\n .passthrough(),\n )\n .describe('Workflow definitions on this page.'),\n })\n .passthrough();\n\ntype WorkflowListTool = EmbeddedWorkflowFeature['listWorkflowsTool'];\ntype WorkflowListInput = Parameters<WorkflowListTool['execute']>[0];\ntype DispatchInput = z.infer<typeof dispatchInputSchema>;\n\nexport interface WorkflowCatalogEntry {\n description: string;\n name: string;\n path: string;\n relativePath: string;\n tags: string[];\n}\n\nexport interface WorkflowCatalogPage {\n directory: string;\n hasNextPage: boolean;\n page: number;\n pageSize: number;\n workflows: WorkflowCatalogEntry[];\n}\n\nexport interface WorkflowLauncherUi {\n editor(title: string, prefill?: string): Promise<string | undefined>;\n input(title: string, placeholder?: string): Promise<string | undefined>;\n notify(message: string, type?: 'info' | 'warning' | 'error'): void;\n select(title: string, options: string[]): Promise<string | undefined>;\n}\n\nexport interface WorkflowLaunchWizardOptions {\n compatibleRunners(workflow: Workflow): string[] | undefined;\n cwd: string;\n launch: WorkflowLaunchExecutor['execute'];\n listWorkflowsTool: WorkflowListTool;\n parseWorkflow(workflowPath: string): Workflow;\n /**\n * Workflow selection, injected rather than taken from `ui.select`: a catalog\n * of this size needs a scrollable, filterable surface, and keeping it out of\n * here leaves the wizard assertable without a terminal.\n */\n pickWorkflow(entries: readonly WorkflowCatalogEntry[]): Promise<WorkflowCatalogEntry | undefined>;\n ui: WorkflowLauncherUi;\n}\n\nfunction hasOwnProperty(value: Record<string, unknown>, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(value, key);\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {};\n}\n\nfunction workflowTriggerBlock(workflow: Workflow): Record<string, unknown> {\n return asRecord(workflow.on);\n}\n\nfunction dispatchInputs(workflow: Workflow): Record<string, DispatchInput> {\n const dispatch = workflowTriggerBlock(workflow)[WORKFLOW_DISPATCH_TRIGGER];\n if (dispatch === null || dispatch === undefined) return {};\n const parsed = z\n .object({\n inputs: z.record(z.string(), dispatchInputSchema).optional().describe('Workflow dispatch input definitions.'),\n })\n .passthrough()\n .safeParse(dispatch);\n if (!parsed.success) throw new Error(`Invalid workflow_dispatch input definitions: ${parsed.error.message}`);\n return parsed.data.inputs ?? {};\n}\n\nfunction launchResultError(result: CallToolResult): Error {\n return new Error(toolResultText(result) || 'Workflow launch failed.');\n}\n\nexport function parseWorkflowCatalogPage(result: CallToolResult): WorkflowCatalogPage {\n if (result.isError) throw launchResultError(result);\n let value: unknown;\n try {\n value = JSON.parse(toolResultText(result));\n } catch (cause) {\n throw new Error('Workflow catalog returned invalid JSON.', { cause });\n }\n const parsed = workflowCatalogPageSchema.safeParse(value);\n if (!parsed.success) throw new Error(`Workflow catalog response was invalid: ${parsed.error.message}`);\n return {\n directory: parsed.data.directory,\n hasNextPage: parsed.data.hasNextPage,\n page: parsed.data.page,\n pageSize: parsed.data.pageSize,\n workflows: parsed.data.workflows.map((entry) => ({\n description: entry.description,\n name: entry.name,\n path: resolve(parsed.data.directory, entry.path),\n relativePath: entry.path,\n tags: entry.tags,\n })),\n };\n}\n\nexport async function loadWorkflowCatalog(\n listWorkflowsTool: WorkflowListTool,\n directory: string,\n): Promise<WorkflowCatalogEntry[]> {\n const entries: WorkflowCatalogEntry[] = [];\n let page = 1;\n let hasNextPage = true;\n while (hasNextPage) {\n const input: WorkflowListInput = { directory, page, pageSize: CATALOG_PAGE_SIZE };\n const result = await listWorkflowsTool.execute(input);\n const catalogPage = parseWorkflowCatalogPage(result);\n entries.push(...catalogPage.workflows);\n hasNextPage = catalogPage.hasNextPage;\n page += 1;\n }\n return entries;\n}\n\nasync function requestPrompt(ui: WorkflowLauncherUi): Promise<string | undefined> {\n while (true) {\n const value = await ui.editor('Workflow prompt');\n if (value === undefined) return undefined;\n const prompt = value.trim();\n if (prompt) return prompt;\n ui.notify('A workflow prompt is required.', 'warning');\n }\n}\n\nasync function requestDispatchInput(\n ui: WorkflowLauncherUi,\n name: string,\n input: DispatchInput,\n): Promise<string | undefined> {\n const title = input.description ? `${name}: ${input.description}` : `Workflow input: ${name}`;\n if (input.options && input.options.length > 0) return ui.select(title, input.options);\n if (input.type === BOOLEAN_INPUT_TYPE) return ui.select(title, ['true', 'false']);\n\n while (true) {\n const value = await ui.input(title, input.default);\n if (value === undefined) return undefined;\n const normalized = value.trim() || input.default;\n if (normalized || !input.required) return normalized || undefined;\n ui.notify(`A value is required for workflow input ${name}.`, 'warning');\n }\n}\n\nexport async function collectWorkflowInputs(\n workflow: Workflow,\n ui: WorkflowLauncherUi,\n): Promise<{ inputs?: Record<string, string>; prompt?: string } | undefined> {\n const triggers = workflowTriggerBlock(workflow);\n const prompt = hasOwnProperty(triggers, USER_PROMPT_TRIGGER) ? await requestPrompt(ui) : undefined;\n if (hasOwnProperty(triggers, USER_PROMPT_TRIGGER) && prompt === undefined) return undefined;\n\n const definitions = dispatchInputs(workflow);\n const inputs: Record<string, string> = {};\n for (const [name, definition] of Object.entries(definitions)) {\n const value = await requestDispatchInput(ui, name, definition);\n if (value === undefined && definition.required) return undefined;\n if (value !== undefined) inputs[name] = value;\n }\n return {\n ...(prompt === undefined ? {} : { prompt }),\n ...(Object.keys(inputs).length === 0 ? {} : { inputs }),\n };\n}\n\nexport async function openWorkflowLaunchWizard(\n options: WorkflowLaunchWizardOptions,\n ctx: ExtensionContext,\n): Promise<CallToolResult | undefined> {\n const entries = await loadWorkflowCatalog(options.listWorkflowsTool, options.cwd);\n if (entries.length === 0) {\n options.ui.notify('No workflow definitions were found in this repository.', 'warning');\n return undefined;\n }\n\n const selected = await options.pickWorkflow(entries);\n if (selected === undefined) return undefined;\n\n const workflow = options.parseWorkflow(selected.path);\n const runners = options.compatibleRunners(workflow);\n if (runners && runners.length === 0) throw new Error('No compatible runner is available for this workflow.');\n let runner: string | undefined;\n if (runners) {\n runner = await options.ui.select('Select a workflow runner', runners);\n if (runner === undefined) return undefined;\n }\n\n const triggerInputs = await collectWorkflowInputs(workflow, options.ui);\n if (triggerInputs === undefined) return undefined;\n const launchInput: WorkflowLaunchInput = {\n workflowPath: selected.path,\n ...(runner === undefined ? {} : { runner }),\n ...(typeof workflow.workspace === 'string' ? { workspace: workflow.workspace } : {}),\n ...triggerInputs,\n };\n return options.launch(launchInput, ctx);\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ let e=require("@earendil-works/pi-tui");var t=class{windowMs;lastEscapeAt;constructor(e=350){this.windowMs=e}observe(t,n=Date.now()){return t===`\x1B\x1B`?(this.lastEscapeAt=void 0,`close`):(0,e.isKeyRelease)(t)?`forward`:(0,e.matchesKey)(t,`escape`)?this.lastEscapeAt!==void 0&&n-this.lastEscapeAt<=this.windowMs?(this.lastEscapeAt=void 0,`close`):(this.lastEscapeAt=n,`forward`):(this.lastEscapeAt=void 0,`forward`)}},n=class{send;flushIntervalMs;pending=``;timer;disposed=!1;constructor(e,t=16){this.send=e,this.flushIntervalMs=t}write(e){this.disposed||e.length===0||(this.pending+=e,!this.timer&&(this.timer=setTimeout(()=>void this.flush(),this.flushIntervalMs),this.timer.unref?.()))}async flush(){this.timer&&clearTimeout(this.timer),this.timer=void 0;let e=this.pending;if(this.pending=``,!(!e||this.disposed))try{await this.send(e)}catch{}}dispose(){this.disposed=!0,this.timer&&clearTimeout(this.timer),this.timer=void 0,this.pending=``}hasPending(){return this.pending.length>0}};exports.DoubleEscapeDetector=t,exports.TerminalInputBatcher=n;
2
+ //# sourceMappingURL=workflowOverlay.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowOverlay.cjs","names":["isKeyRelease","matchesKey"],"sources":["../../src/workflow/workflowOverlay.ts"],"sourcesContent":["/**\n * Input plumbing for the run panel: keystroke batching and the escape hatch.\n *\n * The panel's rendering lives in `workflowRunPanel.ts`, which is a doom overlay;\n * what stays here is the behaviour that has nothing to do with drawing.\n *\n * DESIGN PATTERNS:\n * - Input batching. Every keystroke sent individually costs one subprocess\n * round trip, measured at ~18ms, which caps sustained typing near 56 c/s and\n * makes held keys queue visibly. Coalescing a frame's bytes into one send\n * keeps latency at roughly one frame no matter how fast the user types.\n * - Stateless render. The component recomputes from the latest snapshot on\n * every frame, so a theme change needs no cache invalidation.\n * - Terminal-independent escape hatch. The focused panel forwards every key to\n * the run, so the way out cannot itself depend on a modifier the terminal\n * may not transmit.\n *\n * CODING STANDARDS:\n * - Named exports only\n * - Explicit return types on exported members\n *\n * AVOID:\n * - Sending per keystroke: it is the difference between usable and unusable\n * - Holding the flush timer open after dispose, which would keep writing into\n * a run the user has stopped watching\n */\n\nimport { isKeyRelease, matchesKey } from '@earendil-works/pi-tui';\n\n/** One frame at 60Hz. Long enough to coalesce a burst, short enough to feel direct. */\nconst FLUSH_INTERVAL_MS = 16;\n\n/**\n * How close two Escapes must be to read as \"get me out\".\n *\n * Long enough to be reachable without hurrying, short enough that two\n * deliberate, separate interrupts sent to the run are not mistaken for it.\n */\nexport const DOUBLE_ESCAPE_WINDOW_MS = 350;\n\n/** What the overlay should do with a keystroke. */\nexport type EscapeAction = 'forward' | 'close';\n\n/**\n * The panel's guaranteed way out: press Escape twice, quickly.\n *\n * A focused panel hands every key to the run so its agent stays interruptible,\n * which leaves the two `ctrl+alt` chords as the only exits. Those are exactly\n * the keys a terminal may not deliver: macOS without Option-as-Meta sends plain\n * `ctrl+w` instead, and the byte is forwarded into the run rather than matching.\n * The user is then inside a view they cannot leave. Escape is the one key every\n * terminal transmits identically, which is what makes it the failsafe.\n *\n * The first Escape is still forwarded, so interrupting the step stays as fast as\n * it ever was. Only the second one is consumed, and only when it lands inside\n * the window. The cost is one keystroke: sending the run two literal Escapes in\n * quick succession is no longer possible, which is worth an escapable panel.\n */\nexport class DoubleEscapeDetector {\n private lastEscapeAt: number | undefined;\n\n constructor(private readonly windowMs: number = DOUBLE_ESCAPE_WINDOW_MS) {}\n\n /**\n * Classify one chunk of terminal input.\n *\n * `now` is injected so the decision is a pure function of its inputs, which\n * is what makes the window testable without timers.\n */\n observe(data: string, now: number = Date.now()): EscapeAction {\n // Two Escapes coalesced into one read. A terminal in legacy mode delivers\n // this when they arrive inside the same poll, so it is the same gesture and\n // has to reach the same answer. `matchesKey` never sees it as an escape:\n // it parses as ctrl+alt+[, which has no meaning to a run either way.\n if (data === '\\x1b\\x1b') {\n this.lastEscapeAt = undefined;\n return 'close';\n }\n\n // A Kitty release event repeats the press's own bytes. Counting it would\n // turn one deliberate press into a pair and close the panel under the user.\n if (isKeyRelease(data)) return 'forward';\n\n if (!matchesKey(data, 'escape')) {\n // Anything typed in between makes these two separate interrupts rather\n // than one gesture, so the pair has to start over.\n this.lastEscapeAt = undefined;\n return 'forward';\n }\n\n if (this.lastEscapeAt !== undefined && now - this.lastEscapeAt <= this.windowMs) {\n this.lastEscapeAt = undefined;\n return 'close';\n }\n\n this.lastEscapeAt = now;\n return 'forward';\n }\n}\n\n/**\n * Collects keystrokes and forwards them in frame-sized batches.\n *\n * Failures are swallowed on purpose: a dropped keystroke is a nuisance, but an\n * exception raised from a terminal input handler would tear down the overlay.\n */\nexport class TerminalInputBatcher {\n private pending = '';\n private timer: NodeJS.Timeout | undefined;\n private disposed = false;\n\n constructor(\n private readonly send: (text: string) => Promise<void>,\n private readonly flushIntervalMs: number = FLUSH_INTERVAL_MS,\n ) {}\n\n write(data: string): void {\n if (this.disposed || data.length === 0) return;\n this.pending += data;\n if (this.timer) return;\n this.timer = setTimeout(() => void this.flush(), this.flushIntervalMs);\n this.timer.unref?.();\n }\n\n /** Send whatever has accumulated. Safe to call with nothing pending. */\n async flush(): Promise<void> {\n if (this.timer) clearTimeout(this.timer);\n this.timer = undefined;\n const text = this.pending;\n this.pending = '';\n if (!text || this.disposed) return;\n try {\n await this.send(text);\n } catch {\n // A dropped keystroke must not take the overlay down with it.\n }\n }\n\n /** Stop accepting and forwarding input. Pending bytes are discarded. */\n dispose(): void {\n this.disposed = true;\n if (this.timer) clearTimeout(this.timer);\n this.timer = undefined;\n this.pending = '';\n }\n\n /** Exposed for tests: whether a flush is scheduled. */\n hasPending(): boolean {\n return this.pending.length > 0;\n }\n}\n"],"mappings":"wCA0DA,IAAa,EAAb,KAAkC,CAGH,SAF7B,aAEA,YAAY,EAAiB,IAA4C,CAA5C,KAAA,SAAA,CAA6C,CAQ1E,QAAQ,EAAc,EAAc,KAAK,IAAI,EAAiB,CA2B5D,OAtBI,IAAS,YACX,KAAK,aAAe,IAAA,GACb,UAKT,EAAIA,EAAAA,aAAAA,CAAa,CAAI,EAAU,WAE3B,EAACC,EAAAA,WAAAA,CAAW,EAAM,QAAQ,EAO1B,KAAK,eAAiB,IAAA,IAAa,EAAM,KAAK,cAAgB,KAAK,UACrE,KAAK,aAAe,IAAA,GACb,UAGT,KAAK,aAAe,EACb,YAVL,KAAK,aAAe,IAAA,GACb,UAUX,CACF,EAQa,EAAb,KAAkC,CAMb,KACA,gBANnB,QAAkB,GAClB,MACA,SAAmB,GAEnB,YACE,EACA,EAA2C,GAC3C,CAFiB,KAAA,KAAA,EACA,KAAA,gBAAA,CAChB,CAEH,MAAM,EAAoB,CACpB,KAAK,UAAY,EAAK,SAAW,IACrC,KAAK,SAAW,EACZ,MAAK,QACT,KAAK,MAAQ,eAAiB,KAAK,KAAK,MAAM,EAAG,KAAK,eAAe,EACrE,KAAK,MAAM,QAAQ,GACrB,CAGA,MAAM,OAAuB,CACvB,KAAK,OAAO,aAAa,KAAK,KAAK,EACvC,KAAK,MAAQ,IAAA,GACb,IAAM,EAAO,KAAK,QAClB,QAAK,QAAU,GACX,GAAC,GAAQ,KAAK,UAClB,GAAI,CACF,MAAM,KAAK,KAAK,CAAI,CACtB,MAAQ,CAER,CACF,CAGA,SAAgB,CACd,KAAK,SAAW,GACZ,KAAK,OAAO,aAAa,KAAK,KAAK,EACvC,KAAK,MAAQ,IAAA,GACb,KAAK,QAAU,EACjB,CAGA,YAAsB,CACpB,OAAO,KAAK,QAAQ,OAAS,CAC/B,CACF"}
@@ -0,0 +1,2 @@
1
+ import{isKeyRelease as e,matchesKey as t}from"@earendil-works/pi-tui";var n=class{windowMs;lastEscapeAt;constructor(e=350){this.windowMs=e}observe(n,r=Date.now()){return n===`\x1B\x1B`?(this.lastEscapeAt=void 0,`close`):e(n)?`forward`:t(n,`escape`)?this.lastEscapeAt!==void 0&&r-this.lastEscapeAt<=this.windowMs?(this.lastEscapeAt=void 0,`close`):(this.lastEscapeAt=r,`forward`):(this.lastEscapeAt=void 0,`forward`)}},r=class{send;flushIntervalMs;pending=``;timer;disposed=!1;constructor(e,t=16){this.send=e,this.flushIntervalMs=t}write(e){this.disposed||e.length===0||(this.pending+=e,!this.timer&&(this.timer=setTimeout(()=>void this.flush(),this.flushIntervalMs),this.timer.unref?.()))}async flush(){this.timer&&clearTimeout(this.timer),this.timer=void 0;let e=this.pending;if(this.pending=``,!(!e||this.disposed))try{await this.send(e)}catch{}}dispose(){this.disposed=!0,this.timer&&clearTimeout(this.timer),this.timer=void 0,this.pending=``}hasPending(){return this.pending.length>0}};export{n as DoubleEscapeDetector,r as TerminalInputBatcher};
2
+ //# sourceMappingURL=workflowOverlay.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowOverlay.mjs","names":[],"sources":["../../src/workflow/workflowOverlay.ts"],"sourcesContent":["/**\n * Input plumbing for the run panel: keystroke batching and the escape hatch.\n *\n * The panel's rendering lives in `workflowRunPanel.ts`, which is a doom overlay;\n * what stays here is the behaviour that has nothing to do with drawing.\n *\n * DESIGN PATTERNS:\n * - Input batching. Every keystroke sent individually costs one subprocess\n * round trip, measured at ~18ms, which caps sustained typing near 56 c/s and\n * makes held keys queue visibly. Coalescing a frame's bytes into one send\n * keeps latency at roughly one frame no matter how fast the user types.\n * - Stateless render. The component recomputes from the latest snapshot on\n * every frame, so a theme change needs no cache invalidation.\n * - Terminal-independent escape hatch. The focused panel forwards every key to\n * the run, so the way out cannot itself depend on a modifier the terminal\n * may not transmit.\n *\n * CODING STANDARDS:\n * - Named exports only\n * - Explicit return types on exported members\n *\n * AVOID:\n * - Sending per keystroke: it is the difference between usable and unusable\n * - Holding the flush timer open after dispose, which would keep writing into\n * a run the user has stopped watching\n */\n\nimport { isKeyRelease, matchesKey } from '@earendil-works/pi-tui';\n\n/** One frame at 60Hz. Long enough to coalesce a burst, short enough to feel direct. */\nconst FLUSH_INTERVAL_MS = 16;\n\n/**\n * How close two Escapes must be to read as \"get me out\".\n *\n * Long enough to be reachable without hurrying, short enough that two\n * deliberate, separate interrupts sent to the run are not mistaken for it.\n */\nexport const DOUBLE_ESCAPE_WINDOW_MS = 350;\n\n/** What the overlay should do with a keystroke. */\nexport type EscapeAction = 'forward' | 'close';\n\n/**\n * The panel's guaranteed way out: press Escape twice, quickly.\n *\n * A focused panel hands every key to the run so its agent stays interruptible,\n * which leaves the two `ctrl+alt` chords as the only exits. Those are exactly\n * the keys a terminal may not deliver: macOS without Option-as-Meta sends plain\n * `ctrl+w` instead, and the byte is forwarded into the run rather than matching.\n * The user is then inside a view they cannot leave. Escape is the one key every\n * terminal transmits identically, which is what makes it the failsafe.\n *\n * The first Escape is still forwarded, so interrupting the step stays as fast as\n * it ever was. Only the second one is consumed, and only when it lands inside\n * the window. The cost is one keystroke: sending the run two literal Escapes in\n * quick succession is no longer possible, which is worth an escapable panel.\n */\nexport class DoubleEscapeDetector {\n private lastEscapeAt: number | undefined;\n\n constructor(private readonly windowMs: number = DOUBLE_ESCAPE_WINDOW_MS) {}\n\n /**\n * Classify one chunk of terminal input.\n *\n * `now` is injected so the decision is a pure function of its inputs, which\n * is what makes the window testable without timers.\n */\n observe(data: string, now: number = Date.now()): EscapeAction {\n // Two Escapes coalesced into one read. A terminal in legacy mode delivers\n // this when they arrive inside the same poll, so it is the same gesture and\n // has to reach the same answer. `matchesKey` never sees it as an escape:\n // it parses as ctrl+alt+[, which has no meaning to a run either way.\n if (data === '\\x1b\\x1b') {\n this.lastEscapeAt = undefined;\n return 'close';\n }\n\n // A Kitty release event repeats the press's own bytes. Counting it would\n // turn one deliberate press into a pair and close the panel under the user.\n if (isKeyRelease(data)) return 'forward';\n\n if (!matchesKey(data, 'escape')) {\n // Anything typed in between makes these two separate interrupts rather\n // than one gesture, so the pair has to start over.\n this.lastEscapeAt = undefined;\n return 'forward';\n }\n\n if (this.lastEscapeAt !== undefined && now - this.lastEscapeAt <= this.windowMs) {\n this.lastEscapeAt = undefined;\n return 'close';\n }\n\n this.lastEscapeAt = now;\n return 'forward';\n }\n}\n\n/**\n * Collects keystrokes and forwards them in frame-sized batches.\n *\n * Failures are swallowed on purpose: a dropped keystroke is a nuisance, but an\n * exception raised from a terminal input handler would tear down the overlay.\n */\nexport class TerminalInputBatcher {\n private pending = '';\n private timer: NodeJS.Timeout | undefined;\n private disposed = false;\n\n constructor(\n private readonly send: (text: string) => Promise<void>,\n private readonly flushIntervalMs: number = FLUSH_INTERVAL_MS,\n ) {}\n\n write(data: string): void {\n if (this.disposed || data.length === 0) return;\n this.pending += data;\n if (this.timer) return;\n this.timer = setTimeout(() => void this.flush(), this.flushIntervalMs);\n this.timer.unref?.();\n }\n\n /** Send whatever has accumulated. Safe to call with nothing pending. */\n async flush(): Promise<void> {\n if (this.timer) clearTimeout(this.timer);\n this.timer = undefined;\n const text = this.pending;\n this.pending = '';\n if (!text || this.disposed) return;\n try {\n await this.send(text);\n } catch {\n // A dropped keystroke must not take the overlay down with it.\n }\n }\n\n /** Stop accepting and forwarding input. Pending bytes are discarded. */\n dispose(): void {\n this.disposed = true;\n if (this.timer) clearTimeout(this.timer);\n this.timer = undefined;\n this.pending = '';\n }\n\n /** Exposed for tests: whether a flush is scheduled. */\n hasPending(): boolean {\n return this.pending.length > 0;\n }\n}\n"],"mappings":"sEA0DA,IAAa,EAAb,KAAkC,CAGH,SAF7B,aAEA,YAAY,EAAiB,IAA4C,CAA5C,KAAA,SAAA,CAA6C,CAQ1E,QAAQ,EAAc,EAAc,KAAK,IAAI,EAAiB,CA2B5D,OAtBI,IAAS,YACX,KAAK,aAAe,IAAA,GACb,SAKL,EAAa,CAAI,EAAU,UAE1B,EAAW,EAAM,QAAQ,EAO1B,KAAK,eAAiB,IAAA,IAAa,EAAM,KAAK,cAAgB,KAAK,UACrE,KAAK,aAAe,IAAA,GACb,UAGT,KAAK,aAAe,EACb,YAVL,KAAK,aAAe,IAAA,GACb,UAUX,CACF,EAQa,EAAb,KAAkC,CAMb,KACA,gBANnB,QAAkB,GAClB,MACA,SAAmB,GAEnB,YACE,EACA,EAA2C,GAC3C,CAFiB,KAAA,KAAA,EACA,KAAA,gBAAA,CAChB,CAEH,MAAM,EAAoB,CACpB,KAAK,UAAY,EAAK,SAAW,IACrC,KAAK,SAAW,EACZ,MAAK,QACT,KAAK,MAAQ,eAAiB,KAAK,KAAK,MAAM,EAAG,KAAK,eAAe,EACrE,KAAK,MAAM,QAAQ,GACrB,CAGA,MAAM,OAAuB,CACvB,KAAK,OAAO,aAAa,KAAK,KAAK,EACvC,KAAK,MAAQ,IAAA,GACb,IAAM,EAAO,KAAK,QAClB,QAAK,QAAU,GACX,GAAC,GAAQ,KAAK,UAClB,GAAI,CACF,MAAM,KAAK,KAAK,CAAI,CACtB,MAAQ,CAER,CACF,CAGA,SAAgB,CACd,KAAK,SAAW,GACZ,KAAK,OAAO,aAAa,KAAK,KAAK,EACvC,KAAK,MAAQ,IAAA,GACb,KAAK,QAAU,EACjB,CAGA,YAAsB,CACpB,OAAO,KAAK,QAAQ,OAAS,CAC/B,CACF"}
@@ -0,0 +1,2 @@
1
+ const e=require("./doomOverlay.cjs"),t=require("./overlayText.cjs");let n=require("@earendil-works/pi-tui");e.DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;const r=`selectedBg`;function i(e,n){return n.trim()?e.filter(e=>t.matchesQuery([e.name,e.detail,e.search??``].join(` `),n)):[...e]}var a=class extends e.DoomOverlay{config;rows;done;query=``;selected=0;bodyHeight=1;constructor(e,t,n,r,i){super(e,t),this.config=n,this.rows=r,this.done=i}matches(){return i(this.rows,this.query)}capacity(){return Math.max(1,this.bodyHeight-3)}handleInput(e){if((0,n.matchesKey)(e,`escape`)||(0,n.matchesKey)(e,`ctrl+c`)){this.done(void 0);return}let r=this.matches();if((0,n.matchesKey)(e,`enter`)){let e=r[Math.min(this.selected,r.length-1)];e&&this.done(e.value);return}if((0,n.matchesKey)(e,`up`)||(0,n.matchesKey)(e,`ctrl+p`))this.move(-1,r.length);else if((0,n.matchesKey)(e,`down`)||(0,n.matchesKey)(e,`ctrl+n`))this.move(1,r.length);else if((0,n.matchesKey)(e,`pageUp`))this.move(-this.capacity(),r.length);else if((0,n.matchesKey)(e,`pageDown`))this.move(this.capacity(),r.length);else if((0,n.matchesKey)(e,`backspace`))this.setQuery(this.query.slice(0,-1));else if(e===``)this.setQuery(``);else if(!t.isControlInput(e))this.setQuery(this.query+e);else return;this.tui.requestRender()}setQuery(e){e!==this.query&&(this.query=e,this.selected=0)}move(e,t){t!==0&&(this.selected=Math.max(0,Math.min(t-1,this.selected+e)))}getChrome(){let t=this.matches(),n=this.query?`${t.length} of ${this.rows.length}`:`${this.rows.length}`;return{title:this.config.title,accent:e.DOOM_OVERLAY_ACCENT,breadcrumb:this.config.breadcrumb,headerRight:`${n} ${this.config.unit}`,footer:`↑↓ select · enter ${this.config.action} · type to filter · ctrl+u clear · esc cancel`,footerHints:[[`↑↓`,`select`],[`enter`,this.config.action],[`type`,`filter`],[`ctrl+u`,`clear`],[`esc`,`cancel`]],footerRight:t.length>0?`${Math.min(this.selected,t.length-1)+1}/${t.length}`:`no match`}}renderBody(e,t){return this.bodyHeight=t,[...this.filterField(e),...this.listLines(e)].slice(0,t)}filterField(e){let n=this.query?`${this.query}█`:`█${this.theme.fg(`dim`,this.config.filterPlaceholder)}`;return[this.theme.fg(`dim`,`FILTER`),this.theme.bg(r,t.fit(` ${n}`,e)),``]}listLines(e){if(this.rows.length===0)return[this.theme.fg(`dim`,this.config.emptyMessage)];let n=this.matches();if(n.length===0)return[this.theme.fg(`dim`,`No ${this.config.unit} match "${this.query}".`)];let i=this.capacity(),a=Math.min(this.selected,n.length-1),o=Math.max(0,Math.min(a-i+1,Math.max(0,n.length-i))),s=Math.max(16,Math.floor((e-2-2)*.45)),c=Math.max(0,e-2-2-s);return n.slice(o,o+i).map((n,i)=>{let l=o+i===a,u=l?this.theme.fg(`accent`,`›`):` `,d=n.name||`(unnamed)`,f=l?this.theme.bold(t.fit(d,s)):t.fit(d,s),p=this.theme.fg(`dim`,t.fit(t.pathTail(n.detail,c),c)),m=l?r:`userMessageBg`;return this.theme.bg(m,t.fit(`${u} ${f}${` `.repeat(2)}${p}`,e))})}};async function o(t,n,r){return t.ui.custom((e,t,i,o)=>new a(e,t,n,r,o),e.DOOM_FULLSCREEN_UI_OPTIONS)}exports.WorkflowPickerComponent=a,exports.filterPickerRows=i,exports.openWorkflowPickerOverlay=o;
2
+ //# sourceMappingURL=workflowPicker.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowPicker.cjs","names":["DOOM_FULLSCREEN_UI_OPTIONS","matchesQuery","DoomOverlay","matchesKey","isControlInput","DOOM_OVERLAY_ACCENT","fit","pathTail"],"sources":["../../src/workflow/workflowPicker.ts"],"sourcesContent":["/**\n * Scrollable, filterable list picker for every long list this extension shows.\n *\n * Replaces a bare `ui.select`, which wrapped each `name · detail` pair across\n * two or three terminal rows and left no way to reach a row other than scrolling\n * past all of them. This is a doom overlay instead: one row per item, a windowed\n * list that scrolls with the selection, and a filter field that narrows as you\n * type.\n *\n * Generic over the value it hands back, so `SPC w w` picks a catalog entry and\n * `SPC w r` picks a failed run through the same surface.\n *\n * Rendering is pure, so the surface can be asserted as text without a terminal.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport {\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n DOOM_FULLSCREEN_UI_OPTIONS,\n} from './doomOverlay.ts';\nimport { matchesKey } from '@earendil-works/pi-tui';\nimport { CURSOR_BLOCK, fit, isControlInput, matchesQuery, pathTail, SELECTION_MARKER } from './overlayText';\n\nexport const WORKFLOW_PICKER_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst FILTER_LABEL = 'FILTER';\nconst EMPTY_NAME = '(unnamed)';\nconst KEY_CLEAR = '\\x15';\n/** Rows the filter field and its trailing spacer take from the body. */\nconst FILTER_ROWS = 3;\n/** Columns the marker and its trailing space take from a row. */\nconst MARKER_COLUMNS = 2;\n/** Blank columns between the name column and the detail column. */\nconst COLUMN_GAP = 2;\nconst MIN_NAME_WIDTH = 16;\nconst NAME_RATIO = 0.45;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\nconst DIM: ThemeColor = 'dim';\nconst ACCENT: ThemeColor = 'accent';\nconst SELECTED_BG: ThemeBg = 'selectedBg';\nconst ROW_BG: ThemeBg = 'userMessageBg';\n\nexport interface PickerRow<T> {\n /** Stable identity, used only to keep rows distinct. */\n key: string;\n /** Left column. */\n name: string;\n /** Right column, trimmed from the front so its tail survives. */\n detail: string;\n /** Extra text the filter should match but the row does not show. */\n search?: string;\n value: T;\n}\n\nexport interface WorkflowPickerConfig {\n title: string;\n breadcrumb: string;\n /** Plural noun for the header count, e.g. `workflows`. */\n unit: string;\n /** Verb on the enter key cap, e.g. `launch`. */\n action: string;\n filterPlaceholder: string;\n /** Shown when the source list itself is empty, as opposed to over-filtered. */\n emptyMessage: string;\n}\n\nexport function filterPickerRows<T>(rows: readonly PickerRow<T>[], query: string): PickerRow<T>[] {\n if (!query.trim()) return [...rows];\n return rows.filter((row) => matchesQuery([row.name, row.detail, row.search ?? ''].join(' '), query));\n}\n\nexport class WorkflowPickerComponent<T> extends DoomOverlay {\n private query = '';\n private selected = 0;\n private bodyHeight = 1;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly config: WorkflowPickerConfig,\n private readonly rows: readonly PickerRow<T>[],\n private readonly done: (value: T | undefined) => void,\n ) {\n super(tui, theme);\n }\n\n private matches(): PickerRow<T>[] {\n return filterPickerRows(this.rows, this.query);\n }\n\n /** Rows the list can draw, once the filter field has taken its share. */\n private capacity(): number {\n return Math.max(1, this.bodyHeight - FILTER_ROWS);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n const matches = this.matches();\n if (matchesKey(data, 'enter')) {\n const row = matches[Math.min(this.selected, matches.length - 1)];\n if (row) this.done(row.value);\n return;\n }\n\n if (matchesKey(data, 'up') || matchesKey(data, 'ctrl+p')) this.move(-1, matches.length);\n else if (matchesKey(data, 'down') || matchesKey(data, 'ctrl+n')) this.move(1, matches.length);\n else if (matchesKey(data, 'pageUp')) this.move(-this.capacity(), matches.length);\n else if (matchesKey(data, 'pageDown')) this.move(this.capacity(), matches.length);\n else if (matchesKey(data, 'backspace')) this.setQuery(this.query.slice(0, -1));\n else if (data === KEY_CLEAR) this.setQuery('');\n else if (!isControlInput(data)) this.setQuery(this.query + data);\n else return;\n\n this.tui.requestRender();\n }\n\n /** Editing the filter resets the cursor: the previous index named another row. */\n private setQuery(query: string): void {\n if (query === this.query) return;\n this.query = query;\n this.selected = 0;\n }\n\n private move(delta: number, count: number): void {\n if (count === 0) return;\n this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n }\n\n protected getChrome(): DoomOverlayChrome {\n const matches = this.matches();\n const scope = this.query ? `${matches.length} of ${this.rows.length}` : `${this.rows.length}`;\n return {\n title: this.config.title,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.config.breadcrumb,\n headerRight: `${scope} ${this.config.unit}`,\n footer: `↑↓ select · enter ${this.config.action} · type to filter · ctrl+u clear · esc cancel`,\n footerHints: [\n ['↑↓', 'select'],\n ['enter', this.config.action],\n ['type', 'filter'],\n ['ctrl+u', 'clear'],\n ['esc', 'cancel'],\n ],\n footerRight:\n matches.length > 0 ? `${Math.min(this.selected, matches.length - 1) + 1}/${matches.length}` : 'no match',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n this.bodyHeight = height;\n return [...this.filterField(width), ...this.listLines(width)].slice(0, height);\n }\n\n private filterField(width: number): string[] {\n const value = this.query\n ? `${this.query}${CURSOR_BLOCK}`\n : `${CURSOR_BLOCK}${this.theme.fg(DIM, this.config.filterPlaceholder)}`;\n return [this.theme.fg(DIM, FILTER_LABEL), this.theme.bg(SELECTED_BG, fit(` ${value}`, width)), ''];\n }\n\n private listLines(width: number): string[] {\n if (this.rows.length === 0) return [this.theme.fg(DIM, this.config.emptyMessage)];\n const matches = this.matches();\n if (matches.length === 0) return [this.theme.fg(DIM, `No ${this.config.unit} match \"${this.query}\".`)];\n\n const capacity = this.capacity();\n const active = Math.min(this.selected, matches.length - 1);\n // The window follows the cursor rather than the other way round, so a filter\n // that shrinks the list can never strand the selection off-screen.\n const start = Math.max(0, Math.min(active - capacity + 1, Math.max(0, matches.length - capacity)));\n\n const nameWidth = Math.max(MIN_NAME_WIDTH, Math.floor((width - MARKER_COLUMNS - COLUMN_GAP) * NAME_RATIO));\n const detailWidth = Math.max(0, width - MARKER_COLUMNS - COLUMN_GAP - nameWidth);\n return matches.slice(start, start + capacity).map((row, offset) => {\n const current = start + offset === active;\n const marker = current ? this.theme.fg(ACCENT, SELECTION_MARKER) : ' ';\n const name = row.name || EMPTY_NAME;\n const label = current ? this.theme.bold(fit(name, nameWidth)) : fit(name, nameWidth);\n const detail = this.theme.fg(DIM, fit(pathTail(row.detail, detailWidth), detailWidth));\n const background: ThemeBg = current ? SELECTED_BG : ROW_BG;\n return this.theme.bg(background, fit(`${marker} ${label}${' '.repeat(COLUMN_GAP)}${detail}`, width));\n });\n }\n}\n\nexport async function openWorkflowPickerOverlay<T>(\n ctx: ExtensionContext,\n config: WorkflowPickerConfig,\n rows: readonly PickerRow<T>[],\n): Promise<T | undefined> {\n return ctx.ui.custom<T | undefined>(\n (tui, theme, _keybindings, done) => new WorkflowPickerComponent(tui, theme, config, rows, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{DOOM_FULLSCREEN_UI_OPTIONS as e,DOOM_OVERLAY_ACCENT as t,DoomOverlay as n}from"./doomOverlay.mjs";import{fit as r,isControlInput as i,matchesQuery as a,pathTail as o}from"./overlayText.mjs";import{matchesKey as s}from"@earendil-works/pi-tui";e.overlayOptions;const c=`selectedBg`;function l(e,t){return t.trim()?e.filter(e=>a([e.name,e.detail,e.search??``].join(` `),t)):[...e]}var u=class extends n{config;rows;done;query=``;selected=0;bodyHeight=1;constructor(e,t,n,r,i){super(e,t),this.config=n,this.rows=r,this.done=i}matches(){return l(this.rows,this.query)}capacity(){return Math.max(1,this.bodyHeight-3)}handleInput(e){if(s(e,`escape`)||s(e,`ctrl+c`)){this.done(void 0);return}let t=this.matches();if(s(e,`enter`)){let e=t[Math.min(this.selected,t.length-1)];e&&this.done(e.value);return}if(s(e,`up`)||s(e,`ctrl+p`))this.move(-1,t.length);else if(s(e,`down`)||s(e,`ctrl+n`))this.move(1,t.length);else if(s(e,`pageUp`))this.move(-this.capacity(),t.length);else if(s(e,`pageDown`))this.move(this.capacity(),t.length);else if(s(e,`backspace`))this.setQuery(this.query.slice(0,-1));else if(e===``)this.setQuery(``);else if(!i(e))this.setQuery(this.query+e);else return;this.tui.requestRender()}setQuery(e){e!==this.query&&(this.query=e,this.selected=0)}move(e,t){t!==0&&(this.selected=Math.max(0,Math.min(t-1,this.selected+e)))}getChrome(){let e=this.matches(),n=this.query?`${e.length} of ${this.rows.length}`:`${this.rows.length}`;return{title:this.config.title,accent:t,breadcrumb:this.config.breadcrumb,headerRight:`${n} ${this.config.unit}`,footer:`↑↓ select · enter ${this.config.action} · type to filter · ctrl+u clear · esc cancel`,footerHints:[[`↑↓`,`select`],[`enter`,this.config.action],[`type`,`filter`],[`ctrl+u`,`clear`],[`esc`,`cancel`]],footerRight:e.length>0?`${Math.min(this.selected,e.length-1)+1}/${e.length}`:`no match`}}renderBody(e,t){return this.bodyHeight=t,[...this.filterField(e),...this.listLines(e)].slice(0,t)}filterField(e){let t=this.query?`${this.query}█`:`█${this.theme.fg(`dim`,this.config.filterPlaceholder)}`;return[this.theme.fg(`dim`,`FILTER`),this.theme.bg(c,r(` ${t}`,e)),``]}listLines(e){if(this.rows.length===0)return[this.theme.fg(`dim`,this.config.emptyMessage)];let t=this.matches();if(t.length===0)return[this.theme.fg(`dim`,`No ${this.config.unit} match "${this.query}".`)];let n=this.capacity(),i=Math.min(this.selected,t.length-1),a=Math.max(0,Math.min(i-n+1,Math.max(0,t.length-n))),s=Math.max(16,Math.floor((e-2-2)*.45)),l=Math.max(0,e-2-2-s);return t.slice(a,a+n).map((t,n)=>{let u=a+n===i,d=u?this.theme.fg(`accent`,`›`):` `,f=t.name||`(unnamed)`,p=u?this.theme.bold(r(f,s)):r(f,s),m=this.theme.fg(`dim`,r(o(t.detail,l),l)),h=u?c:`userMessageBg`;return this.theme.bg(h,r(`${d} ${p}${` `.repeat(2)}${m}`,e))})}};async function d(t,n,r){return t.ui.custom((e,t,i,a)=>new u(e,t,n,r,a),e)}export{u as WorkflowPickerComponent,l as filterPickerRows,d as openWorkflowPickerOverlay};
2
+ //# sourceMappingURL=workflowPicker.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowPicker.mjs","names":[],"sources":["../../src/workflow/workflowPicker.ts"],"sourcesContent":["/**\n * Scrollable, filterable list picker for every long list this extension shows.\n *\n * Replaces a bare `ui.select`, which wrapped each `name · detail` pair across\n * two or three terminal rows and left no way to reach a row other than scrolling\n * past all of them. This is a doom overlay instead: one row per item, a windowed\n * list that scrolls with the selection, and a filter field that narrows as you\n * type.\n *\n * Generic over the value it hands back, so `SPC w w` picks a catalog entry and\n * `SPC w r` picks a failed run through the same surface.\n *\n * Rendering is pure, so the surface can be asserted as text without a terminal.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport {\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n DOOM_FULLSCREEN_UI_OPTIONS,\n} from './doomOverlay.ts';\nimport { matchesKey } from '@earendil-works/pi-tui';\nimport { CURSOR_BLOCK, fit, isControlInput, matchesQuery, pathTail, SELECTION_MARKER } from './overlayText';\n\nexport const WORKFLOW_PICKER_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst FILTER_LABEL = 'FILTER';\nconst EMPTY_NAME = '(unnamed)';\nconst KEY_CLEAR = '\\x15';\n/** Rows the filter field and its trailing spacer take from the body. */\nconst FILTER_ROWS = 3;\n/** Columns the marker and its trailing space take from a row. */\nconst MARKER_COLUMNS = 2;\n/** Blank columns between the name column and the detail column. */\nconst COLUMN_GAP = 2;\nconst MIN_NAME_WIDTH = 16;\nconst NAME_RATIO = 0.45;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\nconst DIM: ThemeColor = 'dim';\nconst ACCENT: ThemeColor = 'accent';\nconst SELECTED_BG: ThemeBg = 'selectedBg';\nconst ROW_BG: ThemeBg = 'userMessageBg';\n\nexport interface PickerRow<T> {\n /** Stable identity, used only to keep rows distinct. */\n key: string;\n /** Left column. */\n name: string;\n /** Right column, trimmed from the front so its tail survives. */\n detail: string;\n /** Extra text the filter should match but the row does not show. */\n search?: string;\n value: T;\n}\n\nexport interface WorkflowPickerConfig {\n title: string;\n breadcrumb: string;\n /** Plural noun for the header count, e.g. `workflows`. */\n unit: string;\n /** Verb on the enter key cap, e.g. `launch`. */\n action: string;\n filterPlaceholder: string;\n /** Shown when the source list itself is empty, as opposed to over-filtered. */\n emptyMessage: string;\n}\n\nexport function filterPickerRows<T>(rows: readonly PickerRow<T>[], query: string): PickerRow<T>[] {\n if (!query.trim()) return [...rows];\n return rows.filter((row) => matchesQuery([row.name, row.detail, row.search ?? ''].join(' '), query));\n}\n\nexport class WorkflowPickerComponent<T> extends DoomOverlay {\n private query = '';\n private selected = 0;\n private bodyHeight = 1;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly config: WorkflowPickerConfig,\n private readonly rows: readonly PickerRow<T>[],\n private readonly done: (value: T | undefined) => void,\n ) {\n super(tui, theme);\n }\n\n private matches(): PickerRow<T>[] {\n return filterPickerRows(this.rows, this.query);\n }\n\n /** Rows the list can draw, once the filter field has taken its share. */\n private capacity(): number {\n return Math.max(1, this.bodyHeight - FILTER_ROWS);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n const matches = this.matches();\n if (matchesKey(data, 'enter')) {\n const row = matches[Math.min(this.selected, matches.length - 1)];\n if (row) this.done(row.value);\n return;\n }\n\n if (matchesKey(data, 'up') || matchesKey(data, 'ctrl+p')) this.move(-1, matches.length);\n else if (matchesKey(data, 'down') || matchesKey(data, 'ctrl+n')) this.move(1, matches.length);\n else if (matchesKey(data, 'pageUp')) this.move(-this.capacity(), matches.length);\n else if (matchesKey(data, 'pageDown')) this.move(this.capacity(), matches.length);\n else if (matchesKey(data, 'backspace')) this.setQuery(this.query.slice(0, -1));\n else if (data === KEY_CLEAR) this.setQuery('');\n else if (!isControlInput(data)) this.setQuery(this.query + data);\n else return;\n\n this.tui.requestRender();\n }\n\n /** Editing the filter resets the cursor: the previous index named another row. */\n private setQuery(query: string): void {\n if (query === this.query) return;\n this.query = query;\n this.selected = 0;\n }\n\n private move(delta: number, count: number): void {\n if (count === 0) return;\n this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n }\n\n protected getChrome(): DoomOverlayChrome {\n const matches = this.matches();\n const scope = this.query ? `${matches.length} of ${this.rows.length}` : `${this.rows.length}`;\n return {\n title: this.config.title,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.config.breadcrumb,\n headerRight: `${scope} ${this.config.unit}`,\n footer: `↑↓ select · enter ${this.config.action} · type to filter · ctrl+u clear · esc cancel`,\n footerHints: [\n ['↑↓', 'select'],\n ['enter', this.config.action],\n ['type', 'filter'],\n ['ctrl+u', 'clear'],\n ['esc', 'cancel'],\n ],\n footerRight:\n matches.length > 0 ? `${Math.min(this.selected, matches.length - 1) + 1}/${matches.length}` : 'no match',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n this.bodyHeight = height;\n return [...this.filterField(width), ...this.listLines(width)].slice(0, height);\n }\n\n private filterField(width: number): string[] {\n const value = this.query\n ? `${this.query}${CURSOR_BLOCK}`\n : `${CURSOR_BLOCK}${this.theme.fg(DIM, this.config.filterPlaceholder)}`;\n return [this.theme.fg(DIM, FILTER_LABEL), this.theme.bg(SELECTED_BG, fit(` ${value}`, width)), ''];\n }\n\n private listLines(width: number): string[] {\n if (this.rows.length === 0) return [this.theme.fg(DIM, this.config.emptyMessage)];\n const matches = this.matches();\n if (matches.length === 0) return [this.theme.fg(DIM, `No ${this.config.unit} match \"${this.query}\".`)];\n\n const capacity = this.capacity();\n const active = Math.min(this.selected, matches.length - 1);\n // The window follows the cursor rather than the other way round, so a filter\n // that shrinks the list can never strand the selection off-screen.\n const start = Math.max(0, Math.min(active - capacity + 1, Math.max(0, matches.length - capacity)));\n\n const nameWidth = Math.max(MIN_NAME_WIDTH, Math.floor((width - MARKER_COLUMNS - COLUMN_GAP) * NAME_RATIO));\n const detailWidth = Math.max(0, width - MARKER_COLUMNS - COLUMN_GAP - nameWidth);\n return matches.slice(start, start + capacity).map((row, offset) => {\n const current = start + offset === active;\n const marker = current ? this.theme.fg(ACCENT, SELECTION_MARKER) : ' ';\n const name = row.name || EMPTY_NAME;\n const label = current ? this.theme.bold(fit(name, nameWidth)) : fit(name, nameWidth);\n const detail = this.theme.fg(DIM, fit(pathTail(row.detail, detailWidth), detailWidth));\n const background: ThemeBg = current ? SELECTED_BG : ROW_BG;\n return this.theme.bg(background, fit(`${marker} ${label}${' '.repeat(COLUMN_GAP)}${detail}`, width));\n });\n }\n}\n\nexport async function openWorkflowPickerOverlay<T>(\n ctx: ExtensionContext,\n config: WorkflowPickerConfig,\n rows: readonly PickerRow<T>[],\n): Promise<T | undefined> {\n return ctx.ui.custom<T | undefined>(\n (tui, theme, _keybindings, done) => new WorkflowPickerComponent(tui, theme, config, rows, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require("./doomOverlay.cjs"),t=require("./overlayText.cjs");let n=require("@earendil-works/pi-tui");e.DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;const r=`WORKFLOW PROMPT`,i=`VALUE`,a=`default`,o=[[`↑↓`,`select`],[`enter`,`confirm`],[`esc`,`cancel`]],s=[[`type`,`edit`],[`enter`,`confirm`],[`ctrl+u`,`clear`],[`esc`,`cancel`]],c=`selectedBg`;var l=class extends e.DoomOverlay{options;done;selected;constructor(e,t,n,r){super(e,t),this.options=n,this.done=r,this.selected=Math.max(0,n.preselect?n.choices.indexOf(n.preselect):0)}handleInput(e){if((0,n.matchesKey)(e,`escape`)||(0,n.matchesKey)(e,`ctrl+c`)){this.done(void 0);return}if((0,n.matchesKey)(e,`enter`)){let e=this.options.choices[this.selected];e!==void 0&&this.done(e);return}let t=this.options.choices.length;if(t!==0){if((0,n.matchesKey)(e,`up`)||(0,n.matchesKey)(e,`ctrl+p`))this.selected=Math.max(0,this.selected-1);else if((0,n.matchesKey)(e,`down`)||(0,n.matchesKey)(e,`ctrl+n`))this.selected=Math.min(t-1,this.selected+1);else return;this.tui.requestRender()}}getChrome(){return{title:r,accent:e.DOOM_OVERLAY_ACCENT,breadcrumb:this.options.breadcrumb,headerRight:`${this.options.choices.length} options`,footer:`↑↓ select · enter confirm · esc cancel`,footerHints:o,footerRight:this.options.choices.length>0?`${this.selected+1}/${this.options.choices.length}`:`no options`}}renderBody(e,n){let r=[this.theme.bold(t.fit(this.options.title,e)),``];if(this.options.choices.length===0)return r.push(this.theme.fg(`dim`,`No options are available.`)),r.slice(0,n);r.push(this.theme.fg(`dim`,`CHOOSE`));let i=Math.max(1,n-2-1),o=Math.max(0,Math.min(this.selected-i+1,Math.max(0,this.options.choices.length-i)));for(let[n,s]of this.options.choices.slice(o,o+i).entries()){let i=o+n===this.selected,l=i?this.theme.fg(`accent`,`›`):` `,u=i?this.theme.bold(s):s,d=s===this.options.preselect?this.theme.fg(`dim`,a):``,f=i?c:`userMessageBg`;r.push(this.theme.bg(f,t.rightAligned(`${l} ${u}`,d,e)))}return r.slice(0,n)}},u=class extends e.DoomOverlay{options;done;draft=``;constructor(e,t,n,r){super(e,t),this.options=n,this.done=r}handleInput(e){if((0,n.matchesKey)(e,`escape`)||(0,n.matchesKey)(e,`ctrl+c`)){this.done(void 0);return}if((0,n.matchesKey)(e,`enter`)){this.done(this.draft.trim()||this.options.fallback||``);return}if((0,n.matchesKey)(e,`backspace`))this.draft=this.draft.slice(0,-1);else if(e===``)this.draft=``;else if(!t.isControlInput(e))this.draft+=e;else return;this.tui.requestRender()}getChrome(){return{title:r,accent:e.DOOM_OVERLAY_ACCENT,breadcrumb:this.options.breadcrumb,headerRight:this.options.fallback?`default ${this.options.fallback}`:`no default`,footer:`type to edit · enter confirm · ctrl+u clear · esc cancel`,footerHints:s}}renderBody(e,n){let r=this.options.fallback?t.rightAligned(this.theme.fg(`dim`,i),this.theme.fg(`dim`,`${a} ${this.options.fallback}`),e):this.theme.fg(`dim`,i);return[this.theme.bold(t.fit(this.options.title,e)),``,r,this.theme.bg(c,t.fit(` ${this.draft}█`,e))].slice(0,n)}};async function d(t,n){return t.ui.custom((e,t,r,i)=>new l(e,t,n,i),e.DOOM_FULLSCREEN_UI_OPTIONS)}async function f(t,n){return t.ui.custom((e,t,r,i)=>new u(e,t,n,i),e.DOOM_FULLSCREEN_UI_OPTIONS)}exports.WorkflowChoiceComponent=l,exports.WorkflowInputComponent=u,exports.openWorkflowChoice=d,exports.openWorkflowInput=f;
2
+ //# sourceMappingURL=workflowPrompt.cjs.map