From 366ee1d0fc2c920c583915ec504c921674bda9a7 Mon Sep 17 00:00:00 2001 From: Sorra the Orc Date: Wed, 8 Jul 2026 14:07:18 +0100 Subject: [PATCH 1/8] Add model-display Pi extension (LP-0MR4ZIGDT004A3E1) Adds a Pi extension that shows the resolved provider/model from llama-proxy responses in Pi's status bar. Reads the X-Resolved-Model header added by the proxy and displays it alongside the selected model alias in the format: -> / Registered alongside the existing Worklog extension in package.json. --- packages/tui/extensions/model-display.ts | 61 ++++++++++++++++++++++++ packages/tui/extensions/package.json | 3 +- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 packages/tui/extensions/model-display.ts diff --git a/packages/tui/extensions/model-display.ts b/packages/tui/extensions/model-display.ts new file mode 100644 index 00000000..4b3a61f8 --- /dev/null +++ b/packages/tui/extensions/model-display.ts @@ -0,0 +1,61 @@ +/** + * Model Display Extension (LP-0MR4ZIGDT004A3E1) + * + * Displays the resolved provider/model from llama-proxy responses in Pi's + * status bar. + * + * Shows the format: `/` + * + * - Listens to `after_provider_response` to read the `X-Resolved-Model` header + * that the llama-proxy adds to every proxy-routed request. + * - Listens to `model_select` to track the currently selected Pi model alias. + * - Updates the status bar on every request so the user always sees which + * provider and model actually served their request. + * + * Requires the llama-proxy to be configured as Pi's provider and to include + * the `X-Resolved-Model` header in its responses (see LP-0MR4ZIGDT004A3E1). + */ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +export default function (pi: ExtensionAPI) { + // Track the currently selected model alias (e.g., "plan", "code") + let selectedModel: string | null = null; + + // Track the resolved model from the last proxy response + let resolvedModel: string | null = null; + + // Update the status display + function updateStatus(ctx: { ui: { setStatus: (key: string, value: string) => void } }) { + if (selectedModel && resolvedModel) { + ctx.ui.setStatus("model-display", `${selectedModel} → ${resolvedModel}`); + } else if (selectedModel) { + ctx.ui.setStatus("model-display", `${selectedModel} → (pending)`); + } else if (resolvedModel) { + ctx.ui.setStatus("model-display", `→ ${resolvedModel}`); + } + } + + // Track model selection changes + pi.on("model_select", (_event, ctx) => { + // event.model has provider and id fields + const model = _event.model; + if (model && model.id) { + selectedModel = model.id; + } + updateStatus(ctx); + }); + + // Extract resolved model from proxy response headers + pi.on("after_provider_response", (event, ctx) => { + // event.headers contains the normalized response headers + // X-Resolved-Model header is lowercase in normalized form + const headers = event.headers as Record; + const headerValue: string | undefined = + headers["x-resolved-model"] ?? headers["X-Resolved-Model"]; + + if (headerValue) { + resolvedModel = headerValue; + } + updateStatus(ctx); + }); +} diff --git a/packages/tui/extensions/package.json b/packages/tui/extensions/package.json index dded3f5d..fc32cdb4 100644 --- a/packages/tui/extensions/package.json +++ b/packages/tui/extensions/package.json @@ -6,7 +6,8 @@ "description": "Worklog Pi TUI extension — Work item browser for Pi coding agent", "pi": { "extensions": [ - "./Worklog/index.ts" + "./Worklog/index.ts", + "./model-display.ts" ] } } From 29a15d5e1948f56569c23d4c016c4e3f36182769 Mon Sep 17 00:00:00 2001 From: Sorra the Orc <> Date: Wed, 8 Jul 2026 14:22:36 +0100 Subject: [PATCH 2/8] WL-0MRC3F30E008XJN3: Integrate model-display extension into the Worklog extension - Create Worklog/model-display.ts with moved logic and status key renamed from 'model-display' to 'worklog-model' - Import and register registerModelDisplay() in Worklog/index.ts - Delete packages/tui/extensions/model-display.ts (old standalone extension) - Update package.json to remove ./model-display.ts from extensions list - Add unit tests (15 tests) for registerModelDisplay() covering: - model_select and after_provider_response event registration - Status bar updates with correct formatting - Fallback states (pending model, no model) - Multiple event interactions - Header key case sensitivity --- packages/tui/extensions/Worklog/index.ts | 2 + .../extensions/Worklog/model-display.test.ts | 238 ++++++++++++++++++ .../extensions/{ => Worklog}/model-display.ts | 39 ++- packages/tui/extensions/package.json | 3 +- 4 files changed, 270 insertions(+), 12 deletions(-) create mode 100644 packages/tui/extensions/Worklog/model-display.test.ts rename packages/tui/extensions/{ => Worklog}/model-display.ts (56%) diff --git a/packages/tui/extensions/Worklog/index.ts b/packages/tui/extensions/Worklog/index.ts index 61997898..f852a805 100644 --- a/packages/tui/extensions/Worklog/index.ts +++ b/packages/tui/extensions/Worklog/index.ts @@ -19,6 +19,7 @@ import { worklogConfig } from './config.js'; import { reloadSettings, currentSettings, STAGE_MAP, VALID_STAGES, updateSettings, openSettingsOverlay } from './lib/settings.js'; import { runWl, defaultListWorkItems, defaultListWorkItemsWithStage, createDefaultListWorkItems, createListWorkItemsWithStage, createDefaultListWorkItemsDb, createListWorkItemsWithStageDb, fetchTotalActionableCountDb } from './lib/tools.js'; import { registerAutoInject } from './lib/auto-inject.js'; +import { registerModelDisplay } from './model-display.js'; import { INSTALL_GUARDRAILS } from './lib/guardrails.js'; import { registerSkillPathTool } from './lib/skill-path.js'; import { registerRecoveryModule } from './lib/recovery/register-recovery.js'; @@ -88,6 +89,7 @@ export function createWorklogBrowseExtension(deps: WorklogBrowseDependencies = { return function registerWorklogBrowseExtension(pi: ExtensionAPI): void { registerActivityIndicator(pi, () => currentSettings.showActivityIndicator); registerAutoInject(pi); + registerModelDisplay(pi); INSTALL_GUARDRAILS(pi, { enabled: currentSettings.guardrailsEnabled }); // ── Skill path discovery tool ───────────────────────────────── diff --git a/packages/tui/extensions/Worklog/model-display.test.ts b/packages/tui/extensions/Worklog/model-display.test.ts new file mode 100644 index 00000000..a502a174 --- /dev/null +++ b/packages/tui/extensions/Worklog/model-display.test.ts @@ -0,0 +1,238 @@ +/** + * Unit tests for model-display.ts — Model/provider display in Pi's status bar. + * + * Verifies that: + * 1. registerModelDisplay() sets up event listeners for model_select and + * after_provider_response. + * 2. model_select updates the status bar with the selected model alias. + * 3. after_provider_response updates the status bar with the resolved + * provider/model from X-Resolved-Model header. + * 4. The status bar key is "worklog-model" (not "model-display"). + * 5. Fallback display states work correctly (pending model, no model). + * 6. The module exports the MODEL_DISPLAY_STATUS_KEY constant. + * + * Run: npx vitest run packages/tui/extensions/Worklog/model-display.test.ts + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// ── Mock @earendil-works/pi-coding-agent ────────────────────────────── + +vi.mock('@earendil-works/pi-coding-agent', () => ({ + getAgentDir: () => '/home/test-user/.pi/agent', +})); + +// ── Module under test ───────────────────────────────────────────────── + +import { registerModelDisplay, MODEL_DISPLAY_STATUS_KEY } from './model-display.js'; + +describe('model-display', () => { + /** Tracks event listeners registered by registerModelDisplay. */ + const registeredListeners: Record = {}; + /** Tracks status bar calls: { key: value } */ + const statusCalls: Record = {}; + + /** Mock ExtensionAPI with on() and a minimal context with ui. */ + let mockPi: any; + let mockCtx: any; + + beforeEach(() => { + // Reset tracking + Object.keys(registeredListeners).forEach(k => delete registeredListeners[k]); + Object.keys(statusCalls).forEach(k => delete statusCalls[k]); + + // Mock extension context + mockCtx = { + ui: { + setStatus: vi.fn((key: string, value: string | undefined) => { + statusCalls[key] = value; + }), + }, + }; + + // Mock Pi API + mockPi = { + on: vi.fn((event: string, handler: Function) => { + registeredListeners[event] = handler; + }), + }; + }); + + // ── Exports ─────────────────────────────────────────────────────── + + it('exports MODEL_DISPLAY_STATUS_KEY', () => { + expect(MODEL_DISPLAY_STATUS_KEY).toBe('worklog-model'); + }); + + it('exports registerModelDisplay as a function', () => { + expect(typeof registerModelDisplay).toBe('function'); + }); + + // ── Event registration ─────────────────────────────────────────── + + it('registers model_select listener', () => { + registerModelDisplay(mockPi); + expect(mockPi.on).toHaveBeenCalledWith('model_select', expect.any(Function)); + expect(registeredListeners['model_select']).toBeDefined(); + }); + + it('registers after_provider_response listener', () => { + registerModelDisplay(mockPi); + expect(mockPi.on).toHaveBeenCalledWith('after_provider_response', expect.any(Function)); + expect(registeredListeners['after_provider_response']).toBeDefined(); + }); + + // ── model_select behavior ──────────────────────────────────────── + + it('model_select sets status with selected model id and pending provider', () => { + registerModelDisplay(mockPi); + const handler = registeredListeners['model_select']; + + handler({ model: { id: 'plan', provider: 'openai' } }, mockCtx); + + expect(mockCtx.ui.setStatus).toHaveBeenCalledWith( + 'worklog-model', + 'plan → (pending)', + ); + }); + + it('model_select without model id does not crash', () => { + registerModelDisplay(mockPi); + const handler = registeredListeners['model_select']; + + handler({ model: null }, mockCtx); + + // No status call expected because there's no model to display + expect(mockCtx.ui.setStatus).not.toHaveBeenCalled(); + }); + + it('model_select updates status when after_provider_response was previously received', () => { + registerModelDisplay(mockPi); + const modelHandler = registeredListeners['model_select']; + const responseHandler = registeredListeners['after_provider_response']; + + // First, receive a provider response + responseHandler({ headers: { 'x-resolved-model': 'openai/gpt-4' } }, mockCtx); + expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( + 'worklog-model', + '→ openai/gpt-4', + ); + + // Then, select a model — should show model → provider + modelHandler({ model: { id: 'plan' } }, mockCtx); + expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( + 'worklog-model', + 'plan → openai/gpt-4', + ); + }); + + // ── after_provider_response behavior ────────────────────────────── + + it('after_provider_response extracts X-Resolved-Model from headers', () => { + registerModelDisplay(mockPi); + const handler = registeredListeners['after_provider_response']; + + handler({ headers: { 'x-resolved-model': 'openai/gpt-4' } }, mockCtx); + + expect(mockCtx.ui.setStatus).toHaveBeenCalledWith( + 'worklog-model', + '→ openai/gpt-4', + ); + }); + + it('after_provider_response handles uppercase X-Resolved-Model header', () => { + registerModelDisplay(mockPi); + const handler = registeredListeners['after_provider_response']; + + handler({ headers: { 'X-Resolved-Model': 'anthropic/claude-3' } }, mockCtx); + + expect(mockCtx.ui.setStatus).toHaveBeenCalledWith( + 'worklog-model', + '→ anthropic/claude-3', + ); + }); + + it('after_provider_response without resolved model header does not crash', () => { + registerModelDisplay(mockPi); + const handler = registeredListeners['after_provider_response']; + + handler({ headers: {} }, mockCtx); + + // No call expected because nothing to update if no context is set + expect(mockCtx.ui.setStatus).not.toHaveBeenCalled(); + }); + + it('after_provider_response shows model → provider when model was previously selected', () => { + registerModelDisplay(mockPi); + const modelHandler = registeredListeners['model_select']; + const responseHandler = registeredListeners['after_provider_response']; + + // First, select a model + modelHandler({ model: { id: 'code' } }, mockCtx); + + // Then, receive a provider response + responseHandler({ headers: { 'x-resolved-model': 'anthropic/claude-sonnet-4' } }, mockCtx); + + expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( + 'worklog-model', + 'code → anthropic/claude-sonnet-4', + ); + }); + + // ── Status key verification ─────────────────────────────────────── + + it('uses status key "worklog-model" not "model-display"', () => { + registerModelDisplay(mockPi); + const handler = registeredListeners['model_select']; + + handler({ model: { id: 'plan', provider: 'openai' } }, mockCtx); + + // Verify the old key is NOT used + expect(statusCalls['model-display']).toBeUndefined(); + // Verify the new key IS used + expect(statusCalls['worklog-model']).toBeDefined(); + }); + + // ── Edge cases ─────────────────────────────────────────────────── + + it('handles multiple model_select calls, overwriting previous model', () => { + registerModelDisplay(mockPi); + const handler = registeredListeners['model_select']; + + handler({ model: { id: 'plan' } }, mockCtx); + handler({ model: { id: 'code' } }, mockCtx); + + expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( + 'worklog-model', + 'code → (pending)', + ); + }); + + it('handles multiple after_provider_response calls, overwriting previous resolved model', () => { + registerModelDisplay(mockPi); + const modelHandler = registeredListeners['model_select']; + const responseHandler = registeredListeners['after_provider_response']; + + modelHandler({ model: { id: 'code' } }, mockCtx); + responseHandler({ headers: { 'x-resolved-model': 'openai/gpt-4' } }, mockCtx); + responseHandler({ headers: { 'x-resolved-model': 'anthropic/claude-sonnet-4' } }, mockCtx); + + expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( + 'worklog-model', + 'code → anthropic/claude-sonnet-4', + ); + }); + + it('after_provider_response with selectedModel=null shows → provider only', () => { + registerModelDisplay(mockPi); + const handler = registeredListeners['after_provider_response']; + + // Without any prior model_select + handler({ headers: { 'x-resolved-model': 'openai/gpt-4' } }, mockCtx); + + expect(mockCtx.ui.setStatus).toHaveBeenCalledWith( + 'worklog-model', + '→ openai/gpt-4', + ); + }); +}); diff --git a/packages/tui/extensions/model-display.ts b/packages/tui/extensions/Worklog/model-display.ts similarity index 56% rename from packages/tui/extensions/model-display.ts rename to packages/tui/extensions/Worklog/model-display.ts index 4b3a61f8..6e00ccf9 100644 --- a/packages/tui/extensions/model-display.ts +++ b/packages/tui/extensions/Worklog/model-display.ts @@ -1,8 +1,9 @@ /** - * Model Display Extension (LP-0MR4ZIGDT004A3E1) + * Model display module for the Worklog Pi extension. * * Displays the resolved provider/model from llama-proxy responses in Pi's - * status bar. + * status bar. Integrated from the standalone model-display extension + * (previously at packages/tui/extensions/model-display.ts). * * Shows the format: `/` * @@ -15,9 +16,27 @@ * Requires the llama-proxy to be configured as Pi's provider and to include * the `X-Resolved-Model` header in its responses (see LP-0MR4ZIGDT004A3E1). */ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -export default function (pi: ExtensionAPI) { +import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; + +/** + * Status key used for the model/provider display in the footer. + * Changed from "model-display" when this module was integrated into the + * Worklog extension, to avoid conflicts with other status entries. + */ +export const MODEL_DISPLAY_STATUS_KEY = 'worklog-model'; + +/** + * Register model-display event handlers with a Pi extension instance. + * + * Sets up listeners for: + * - `model_select` — tracks the currently selected Pi model alias + * - `after_provider_response` — reads the `X-Resolved-Model` header from + * llama-proxy responses and updates the status bar + * + * @param pi - The ExtensionAPI instance + */ +export function registerModelDisplay(pi: ExtensionAPI): void { // Track the currently selected model alias (e.g., "plan", "code") let selectedModel: string | null = null; @@ -27,16 +46,16 @@ export default function (pi: ExtensionAPI) { // Update the status display function updateStatus(ctx: { ui: { setStatus: (key: string, value: string) => void } }) { if (selectedModel && resolvedModel) { - ctx.ui.setStatus("model-display", `${selectedModel} → ${resolvedModel}`); + ctx.ui.setStatus(MODEL_DISPLAY_STATUS_KEY, `${selectedModel} → ${resolvedModel}`); } else if (selectedModel) { - ctx.ui.setStatus("model-display", `${selectedModel} → (pending)`); + ctx.ui.setStatus(MODEL_DISPLAY_STATUS_KEY, `${selectedModel} → (pending)`); } else if (resolvedModel) { - ctx.ui.setStatus("model-display", `→ ${resolvedModel}`); + ctx.ui.setStatus(MODEL_DISPLAY_STATUS_KEY, `→ ${resolvedModel}`); } } // Track model selection changes - pi.on("model_select", (_event, ctx) => { + pi.on('model_select', (_event, ctx) => { // event.model has provider and id fields const model = _event.model; if (model && model.id) { @@ -46,12 +65,12 @@ export default function (pi: ExtensionAPI) { }); // Extract resolved model from proxy response headers - pi.on("after_provider_response", (event, ctx) => { + pi.on('after_provider_response', (event, ctx) => { // event.headers contains the normalized response headers // X-Resolved-Model header is lowercase in normalized form const headers = event.headers as Record; const headerValue: string | undefined = - headers["x-resolved-model"] ?? headers["X-Resolved-Model"]; + headers['x-resolved-model'] ?? headers['X-Resolved-Model']; if (headerValue) { resolvedModel = headerValue; diff --git a/packages/tui/extensions/package.json b/packages/tui/extensions/package.json index fc32cdb4..dded3f5d 100644 --- a/packages/tui/extensions/package.json +++ b/packages/tui/extensions/package.json @@ -6,8 +6,7 @@ "description": "Worklog Pi TUI extension — Work item browser for Pi coding agent", "pi": { "extensions": [ - "./Worklog/index.ts", - "./model-display.ts" + "./Worklog/index.ts" ] } } From 2a34e714f206642c44d41d6cf82e78db858cec5a Mon Sep 17 00:00:00 2001 From: Sorra the Orc <> Date: Wed, 8 Jul 2026 17:11:57 +0100 Subject: [PATCH 3/8] WL-0MRC91VIN0089UXD: Show only provider/model in status bar, remove model alias prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes the Worklog extension model status bar display from '/' to just '/'. - Removed model alias display (e.g., 'plan' prefix) from status bar - Changed (pending) fallback from ' → (pending)' to '(pending)' - Removed '→' arrow prefix from provider-only display - model_select listener now only tracks model presence for (pending) state - Updated all 15 tests to reflect new display format - No changes to X-Resolved-Model header parsing or event listeners --- .../extensions/Worklog/model-display.test.ts | 44 +++++++++---------- .../tui/extensions/Worklog/model-display.ts | 25 ++++++----- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/packages/tui/extensions/Worklog/model-display.test.ts b/packages/tui/extensions/Worklog/model-display.test.ts index a502a174..b03e0183 100644 --- a/packages/tui/extensions/Worklog/model-display.test.ts +++ b/packages/tui/extensions/Worklog/model-display.test.ts @@ -4,11 +4,11 @@ * Verifies that: * 1. registerModelDisplay() sets up event listeners for model_select and * after_provider_response. - * 2. model_select updates the status bar with the selected model alias. + * 2. The status display shows only the resolved provider/model (no model alias). * 3. after_provider_response updates the status bar with the resolved * provider/model from X-Resolved-Model header. - * 4. The status bar key is "worklog-model" (not "model-display"). - * 5. Fallback display states work correctly (pending model, no model). + * 4. When no proxy response has been received yet, the status bar shows (pending). + * 5. The status bar key is "worklog-model" (not "model-display"). * 6. The module exports the MODEL_DISPLAY_STATUS_KEY constant. * * Run: npx vitest run packages/tui/extensions/Worklog/model-display.test.ts @@ -84,7 +84,7 @@ describe('model-display', () => { // ── model_select behavior ──────────────────────────────────────── - it('model_select sets status with selected model id and pending provider', () => { + it('model_select sets status to (pending) when no response yet', () => { registerModelDisplay(mockPi); const handler = registeredListeners['model_select']; @@ -92,21 +92,21 @@ describe('model-display', () => { expect(mockCtx.ui.setStatus).toHaveBeenCalledWith( 'worklog-model', - 'plan → (pending)', + '(pending)', ); }); - it('model_select without model id does not crash', () => { + it('model_select without model id does not crash and does not set status', () => { registerModelDisplay(mockPi); const handler = registeredListeners['model_select']; handler({ model: null }, mockCtx); - // No status call expected because there's no model to display + // No status call expected because there's nothing to display expect(mockCtx.ui.setStatus).not.toHaveBeenCalled(); }); - it('model_select updates status when after_provider_response was previously received', () => { + it('model_select no longer shows model alias when after_provider_response was previously received', () => { registerModelDisplay(mockPi); const modelHandler = registeredListeners['model_select']; const responseHandler = registeredListeners['after_provider_response']; @@ -115,20 +115,20 @@ describe('model-display', () => { responseHandler({ headers: { 'x-resolved-model': 'openai/gpt-4' } }, mockCtx); expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( 'worklog-model', - '→ openai/gpt-4', + 'openai/gpt-4', ); - // Then, select a model — should show model → provider + // Then, select a model — should still show provider/model only (no model alias) modelHandler({ model: { id: 'plan' } }, mockCtx); expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( 'worklog-model', - 'plan → openai/gpt-4', + 'openai/gpt-4', ); }); // ── after_provider_response behavior ────────────────────────────── - it('after_provider_response extracts X-Resolved-Model from headers', () => { + it('after_provider_response extracts X-Resolved-Model from headers (no arrow prefix)', () => { registerModelDisplay(mockPi); const handler = registeredListeners['after_provider_response']; @@ -136,7 +136,7 @@ describe('model-display', () => { expect(mockCtx.ui.setStatus).toHaveBeenCalledWith( 'worklog-model', - '→ openai/gpt-4', + 'openai/gpt-4', ); }); @@ -148,7 +148,7 @@ describe('model-display', () => { expect(mockCtx.ui.setStatus).toHaveBeenCalledWith( 'worklog-model', - '→ anthropic/claude-3', + 'anthropic/claude-3', ); }); @@ -158,11 +158,11 @@ describe('model-display', () => { handler({ headers: {} }, mockCtx); - // No call expected because nothing to update if no context is set + // No call expected because nothing to update expect(mockCtx.ui.setStatus).not.toHaveBeenCalled(); }); - it('after_provider_response shows model → provider when model was previously selected', () => { + it('after_provider_response shows provider/model (no model alias) when model was previously selected', () => { registerModelDisplay(mockPi); const modelHandler = registeredListeners['model_select']; const responseHandler = registeredListeners['after_provider_response']; @@ -175,7 +175,7 @@ describe('model-display', () => { expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( 'worklog-model', - 'code → anthropic/claude-sonnet-4', + 'anthropic/claude-sonnet-4', ); }); @@ -195,7 +195,7 @@ describe('model-display', () => { // ── Edge cases ─────────────────────────────────────────────────── - it('handles multiple model_select calls, overwriting previous model', () => { + it('handles multiple model_select calls, staying in pending state', () => { registerModelDisplay(mockPi); const handler = registeredListeners['model_select']; @@ -204,7 +204,7 @@ describe('model-display', () => { expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( 'worklog-model', - 'code → (pending)', + '(pending)', ); }); @@ -219,11 +219,11 @@ describe('model-display', () => { expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( 'worklog-model', - 'code → anthropic/claude-sonnet-4', + 'anthropic/claude-sonnet-4', ); }); - it('after_provider_response with selectedModel=null shows → provider only', () => { + it('after_provider_response with no prior model_select shows provider/model only', () => { registerModelDisplay(mockPi); const handler = registeredListeners['after_provider_response']; @@ -232,7 +232,7 @@ describe('model-display', () => { expect(mockCtx.ui.setStatus).toHaveBeenCalledWith( 'worklog-model', - '→ openai/gpt-4', + 'openai/gpt-4', ); }); }); diff --git a/packages/tui/extensions/Worklog/model-display.ts b/packages/tui/extensions/Worklog/model-display.ts index 6e00ccf9..8aeac7c5 100644 --- a/packages/tui/extensions/Worklog/model-display.ts +++ b/packages/tui/extensions/Worklog/model-display.ts @@ -5,11 +5,13 @@ * status bar. Integrated from the standalone model-display extension * (previously at packages/tui/extensions/model-display.ts). * - * Shows the format: `/` + * Shows the format: `/` (e.g., `openai/gpt-4`) + * without the Pi model alias prefix. * * - Listens to `after_provider_response` to read the `X-Resolved-Model` header * that the llama-proxy adds to every proxy-routed request. - * - Listens to `model_select` to track the currently selected Pi model alias. + * - Listens to `model_select` to track whether a model is selected (for the + * `(pending)` fallback state), but does not display the model alias. * - Updates the status bar on every request so the user always sees which * provider and model actually served their request. * @@ -30,14 +32,18 @@ export const MODEL_DISPLAY_STATUS_KEY = 'worklog-model'; * Register model-display event handlers with a Pi extension instance. * * Sets up listeners for: - * - `model_select` — tracks the currently selected Pi model alias + * - `model_select` — tracks whether a model is selected (for `(pending)` state) * - `after_provider_response` — reads the `X-Resolved-Model` header from * llama-proxy responses and updates the status bar * + * The status display shows only the resolved provider/model from the + * `X-Resolved-Model` header. When no response has been received yet but a + * model is selected, it shows `(pending)`. The Pi model alias is not shown. + * * @param pi - The ExtensionAPI instance */ export function registerModelDisplay(pi: ExtensionAPI): void { - // Track the currently selected model alias (e.g., "plan", "code") + // Track whether a model is selected (used for the (pending) fallback state) let selectedModel: string | null = null; // Track the resolved model from the last proxy response @@ -45,16 +51,15 @@ export function registerModelDisplay(pi: ExtensionAPI): void { // Update the status display function updateStatus(ctx: { ui: { setStatus: (key: string, value: string) => void } }) { - if (selectedModel && resolvedModel) { - ctx.ui.setStatus(MODEL_DISPLAY_STATUS_KEY, `${selectedModel} → ${resolvedModel}`); + if (resolvedModel) { + ctx.ui.setStatus(MODEL_DISPLAY_STATUS_KEY, resolvedModel); } else if (selectedModel) { - ctx.ui.setStatus(MODEL_DISPLAY_STATUS_KEY, `${selectedModel} → (pending)`); - } else if (resolvedModel) { - ctx.ui.setStatus(MODEL_DISPLAY_STATUS_KEY, `→ ${resolvedModel}`); + ctx.ui.setStatus(MODEL_DISPLAY_STATUS_KEY, '(pending)'); } + // If neither resolvedModel nor selectedModel, no status call (nothing to display) } - // Track model selection changes + // Track model selection changes (used only for (pending) fallback) pi.on('model_select', (_event, ctx) => { // event.model has provider and id fields const model = _event.model; From f0fad16410901724fe4508d15cebaf2391f22e87 Mon Sep 17 00:00:00 2001 From: Sorra the Orc <> Date: Wed, 8 Jul 2026 23:30:45 +0100 Subject: [PATCH 4/8] Add audit approve/reject shortcuts and update docs - Add a+y (audit approve) and a+r (audit reject) chord shortcuts - Rename a single-key audit to a,a chord (audit automatic) - Add shortcuts.json source path note to README.md - Document --ready-to-close yes/no usage in AUDIT_STATUS.md - Bump version to 1.0.1 - Update tests for new entry/chord counts and removed a key --- docs/AUDIT_STATUS.md | 9 +++++++ packages/tui/extensions/README.md | 3 +++ .../Worklog/shortcut-config.test.ts | 25 ++++++------------- .../tui/extensions/Worklog/shortcuts.json | 20 +++++++++++++-- src/version.ts | 2 +- tests/cli/status.test.ts | 2 +- 6 files changed, 40 insertions(+), 21 deletions(-) diff --git a/docs/AUDIT_STATUS.md b/docs/AUDIT_STATUS.md index 591cd694..ebd5eb3f 100644 --- a/docs/AUDIT_STATUS.md +++ b/docs/AUDIT_STATUS.md @@ -34,8 +34,17 @@ All acceptance criteria verified." # Set audit via the new dedicated command wl audit-set SA-123 --ready-to-close --summary "All acceptance criteria verified." --author agent + +# Mark an audit as approved by manual review +wl audit-set SA-123 --ready-to-close yes --summary "Manual review approved." --author "reviewer-name" + +# Mark an audit as not yet ready (e.g., after a failed review) +wl audit-set SA-123 --ready-to-close no --summary "Manual review failed: criteria X not met." ``` +> **Note:** `--ready-to-close` accepts both a bare flag (`--ready-to-close`) which defaults to `yes`, +> and an explicit value (`--ready-to-close yes` or `--ready-to-close no`). + ### Viewing audit results ```bash diff --git a/packages/tui/extensions/README.md b/packages/tui/extensions/README.md index 6732ed9a..7011ac60 100644 --- a/packages/tui/extensions/README.md +++ b/packages/tui/extensions/README.md @@ -382,6 +382,9 @@ The `/wl` command registers `getArgumentCompletions`, so Pi's editor shows autoc ## Shortcuts +Source config file: `packages/tui/extensions/Worklog/shortcuts.json` +(installed to `~/.pi/agent/extensions/worklog/Worklog/shortcuts.json`). + The `shortcuts.json` config file defines a **config-driven shortcut system** that allows keyboard shortcuts in the Pi extension's worklog browse views (list and detail) to be expressed declaratively rather than hardcoded. ### Schema diff --git a/packages/tui/extensions/Worklog/shortcut-config.test.ts b/packages/tui/extensions/Worklog/shortcut-config.test.ts index 5b27c7f9..a6727cef 100644 --- a/packages/tui/extensions/Worklog/shortcut-config.test.ts +++ b/packages/tui/extensions/Worklog/shortcut-config.test.ts @@ -210,7 +210,7 @@ describe('loadShortcutConfig', () => { it('loads valid entries from shortcuts.json', () => { const registry = loadShortcutConfig(); const entries = registry.getEntries(); - expect(entries).toHaveLength(15); + expect(entries).toHaveLength(17); const createEntry = entries.find(e => e.key === 'c'); expect(createEntry).toBeDefined(); @@ -244,15 +244,7 @@ describe('loadShortcutConfig', () => { expect(intakeEntry!.label).toBe('intake'); expect(intakeEntry!.description).toBe('Ensure that the selected item is reasonably well defined in terms of objectives.'); - const auditEntry = entries.find(e => e.key === 'a'); - expect(auditEntry).toBeDefined(); - expect(auditEntry!.command).toBe('/skill:audit '); - expect(auditEntry!.view).toBe('both'); - expect(auditEntry!.stages).toEqual(['in_progress', 'in_review']); - expect(auditEntry!.label).toBe('audit'); - expect(auditEntry!.description).toBe('Run an audit on the selected work item'); - - expect(entries.filter(e => e.key === '').length).toBe(9); // 9 chord entries have empty key + expect(entries.filter(e => e.key === '').length).toBe(12); // 12 chord entries have empty key }); it('has no duplicate key+view or chord+view combinations in shortcuts.json', () => { @@ -297,18 +289,17 @@ describe('loadShortcutConfig', () => { expect(registry.lookup('n', 'detail', 'idea')).toBe('/intake '); expect(registry.lookup('n', 'detail', 'intake_complete')).toBeUndefined(); - // 'a' (audit) has stages: ['in_progress', 'in_review'] - expect(registry.lookup('a', 'list', 'in_review')).toBe('/skill:audit '); - expect(registry.lookup('a', 'detail', 'in_review')).toBe('/skill:audit '); - expect(registry.lookup('a', 'list', 'in_progress')).toBe('/skill:audit '); - expect(registry.lookup('a', 'list', 'idea')).toBeUndefined(); + // 'a' is now a chord leader (a,a for audit, a+y for approve, a+r for reject), + // so single-key lookup for 'a' returns undefined + expect(registry.lookup('a', 'list', 'in_review')).toBeUndefined(); + expect(registry.lookup('a', 'detail', 'in_review')).toBeUndefined(); + expect(registry.lookup('a', 'list', 'in_progress')).toBeUndefined(); // Without stage parameter, entries with stages constraint still work // (backward compatible when calling without stage) expect(registry.lookup('n', 'detail')).toBe('/intake '); expect(registry.lookup('i', 'list')).toBe('/skill:implement '); expect(registry.lookup('p', 'list')).toBe('/plan '); - expect(registry.lookup('a', 'list')).toBe('/skill:audit '); }); it('loads chord entries from shortcuts.json', () => { @@ -316,7 +307,7 @@ describe('loadShortcutConfig', () => { const entries = registry.getEntries(); const upChords = registry.getChordEntries(); - expect(upChords).toHaveLength(9); + expect(upChords).toHaveLength(12); const upEntry = upChords.find((e: any) => Array.isArray((e as any).chord) && (e as any).chord[0] === 'u' && (e as any).chord[1] === 'p', diff --git a/packages/tui/extensions/Worklog/shortcuts.json b/packages/tui/extensions/Worklog/shortcuts.json index e6ad9611..f36582f2 100644 --- a/packages/tui/extensions/Worklog/shortcuts.json +++ b/packages/tui/extensions/Worklog/shortcuts.json @@ -29,13 +29,29 @@ "stages": ["intake_complete", "plan_complete", "in_progress"], "label": "implement", "description": "Run the implement workflow on the selected work item" + }, + { + "chord": ["a", "y"], + "command": "!!wl audit-set --ready-to-close yes --summary 'Approved by manual review'", + "view": "both", + "stages": ["in_review"], + "label": "audit approve", + "description": "Approve the in_review item by manual audit." + }, + { + "chord": ["a", "r"], + "command": "!!wl audit-set --ready-to-close no --summary 'Rejected by manual review. '", + "view": "both", + "stages": ["in_review"], + "label": "audit reject", + "description": "Reject the in_review item by manual audit." }, { - "key": "a", + "chord": ["a", "a"], "command": "/skill:audit ", "view": "both", "stages": ["in_progress", "in_review"], - "label": "audit", + "label": "audit automatic", "description": "Run an audit on the selected work item" }, { diff --git a/src/version.ts b/src/version.ts index 14260576..50fff331 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ // Auto-generated; do not edit. -export const WORKLOG_VERSION = '1.0.0'; +export const WORKLOG_VERSION = '1.0.1'; diff --git a/tests/cli/status.test.ts b/tests/cli/status.test.ts index cd5b60da..5d0abe00 100644 --- a/tests/cli/status.test.ts +++ b/tests/cli/status.test.ts @@ -108,7 +108,7 @@ describe('CLI Status Tests', () => { expect(stdout).toContain('Worklog System Status'); expect(stdout).toContain('Initialized: Yes'); - expect(stdout).toContain('Version: 1.0.0'); + expect(stdout).toContain('Version: 1.0.1'); expect(stdout).toContain('Configuration:'); expect(stdout).toContain('Database Summary:'); expect(stdout).toContain('Work Items:'); From c2848861f240f64928883a5a4b45cdd051230764 Mon Sep 17 00:00:00 2001 From: Sorra the Orc <> Date: Wed, 8 Jul 2026 23:48:51 +0100 Subject: [PATCH 5/8] WL-0MRC91VIN0089UXD: Ensure provider/model sorts first in extension status line by renaming key to worklog-0model Pi renders extension statuses sorted alphabetically by key. The previous key 'worklog-model' sorted last (after worklog-activity and worklog-auto-inject), leaving the provider/model at the end of the extension status line. Renamed to 'worklog-0model' so '0' < 'a' in localeCompare, placing the provider/model at the start of the line. Also updated all 15 test assertions and the export constant. --- .../extensions/Worklog/model-display.test.ts | 27 ++++++++++--------- .../tui/extensions/Worklog/model-display.ts | 4 ++- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/tui/extensions/Worklog/model-display.test.ts b/packages/tui/extensions/Worklog/model-display.test.ts index b03e0183..b76655b4 100644 --- a/packages/tui/extensions/Worklog/model-display.test.ts +++ b/packages/tui/extensions/Worklog/model-display.test.ts @@ -8,7 +8,8 @@ * 3. after_provider_response updates the status bar with the resolved * provider/model from X-Resolved-Model header. * 4. When no proxy response has been received yet, the status bar shows (pending). - * 5. The status bar key is "worklog-model" (not "model-display"). + * 5. The status bar key is "worklog-0model" (not "model-display"), + * with the `0` prefix ensuring it sorts first in the extension status line. * 6. The module exports the MODEL_DISPLAY_STATUS_KEY constant. * * Run: npx vitest run packages/tui/extensions/Worklog/model-display.test.ts @@ -61,7 +62,7 @@ describe('model-display', () => { // ── Exports ─────────────────────────────────────────────────────── it('exports MODEL_DISPLAY_STATUS_KEY', () => { - expect(MODEL_DISPLAY_STATUS_KEY).toBe('worklog-model'); + expect(MODEL_DISPLAY_STATUS_KEY).toBe('worklog-0model'); }); it('exports registerModelDisplay as a function', () => { @@ -91,7 +92,7 @@ describe('model-display', () => { handler({ model: { id: 'plan', provider: 'openai' } }, mockCtx); expect(mockCtx.ui.setStatus).toHaveBeenCalledWith( - 'worklog-model', + 'worklog-0model', '(pending)', ); }); @@ -114,14 +115,14 @@ describe('model-display', () => { // First, receive a provider response responseHandler({ headers: { 'x-resolved-model': 'openai/gpt-4' } }, mockCtx); expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( - 'worklog-model', + 'worklog-0model', 'openai/gpt-4', ); // Then, select a model — should still show provider/model only (no model alias) modelHandler({ model: { id: 'plan' } }, mockCtx); expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( - 'worklog-model', + 'worklog-0model', 'openai/gpt-4', ); }); @@ -135,7 +136,7 @@ describe('model-display', () => { handler({ headers: { 'x-resolved-model': 'openai/gpt-4' } }, mockCtx); expect(mockCtx.ui.setStatus).toHaveBeenCalledWith( - 'worklog-model', + 'worklog-0model', 'openai/gpt-4', ); }); @@ -147,7 +148,7 @@ describe('model-display', () => { handler({ headers: { 'X-Resolved-Model': 'anthropic/claude-3' } }, mockCtx); expect(mockCtx.ui.setStatus).toHaveBeenCalledWith( - 'worklog-model', + 'worklog-0model', 'anthropic/claude-3', ); }); @@ -174,14 +175,14 @@ describe('model-display', () => { responseHandler({ headers: { 'x-resolved-model': 'anthropic/claude-sonnet-4' } }, mockCtx); expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( - 'worklog-model', + 'worklog-0model', 'anthropic/claude-sonnet-4', ); }); // ── Status key verification ─────────────────────────────────────── - it('uses status key "worklog-model" not "model-display"', () => { + it('uses status key "worklog-0model" (sorts first) not "model-display"', () => { registerModelDisplay(mockPi); const handler = registeredListeners['model_select']; @@ -190,7 +191,7 @@ describe('model-display', () => { // Verify the old key is NOT used expect(statusCalls['model-display']).toBeUndefined(); // Verify the new key IS used - expect(statusCalls['worklog-model']).toBeDefined(); + expect(statusCalls['worklog-0model']).toBeDefined(); }); // ── Edge cases ─────────────────────────────────────────────────── @@ -203,7 +204,7 @@ describe('model-display', () => { handler({ model: { id: 'code' } }, mockCtx); expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( - 'worklog-model', + 'worklog-0model', '(pending)', ); }); @@ -218,7 +219,7 @@ describe('model-display', () => { responseHandler({ headers: { 'x-resolved-model': 'anthropic/claude-sonnet-4' } }, mockCtx); expect(mockCtx.ui.setStatus).toHaveBeenLastCalledWith( - 'worklog-model', + 'worklog-0model', 'anthropic/claude-sonnet-4', ); }); @@ -231,7 +232,7 @@ describe('model-display', () => { handler({ headers: { 'x-resolved-model': 'openai/gpt-4' } }, mockCtx); expect(mockCtx.ui.setStatus).toHaveBeenCalledWith( - 'worklog-model', + 'worklog-0model', 'openai/gpt-4', ); }); diff --git a/packages/tui/extensions/Worklog/model-display.ts b/packages/tui/extensions/Worklog/model-display.ts index 8aeac7c5..31a0e2b0 100644 --- a/packages/tui/extensions/Worklog/model-display.ts +++ b/packages/tui/extensions/Worklog/model-display.ts @@ -23,10 +23,12 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; /** * Status key used for the model/provider display in the footer. + * The `0` prefix ensures this status sorts first in the extension status + * line (localeCompare order), placing the provider/model at the start. * Changed from "model-display" when this module was integrated into the * Worklog extension, to avoid conflicts with other status entries. */ -export const MODEL_DISPLAY_STATUS_KEY = 'worklog-model'; +export const MODEL_DISPLAY_STATUS_KEY = 'worklog-0model'; /** * Register model-display event handlers with a Pi extension instance. From e403b257db9a778ad4dbba0f4e6b37976a139677 Mon Sep 17 00:00:00 2001 From: Sorra the Orc <> Date: Wed, 8 Jul 2026 23:51:15 +0100 Subject: [PATCH 6/8] =?UTF-8?q?WL-0MRCN2HF5003HSG9:=20Change=20audit=20que?= =?UTF-8?q?stion=20mark=20icon=20from=20red=20(=E2=9D=93)=20to=20grey=20(?= =?UTF-8?q?=E2=9D=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - src/icons.ts: Changed AUDIT_ICON.unknown from \u{2753} (❓, red question mark) to \u{2754} (❔, white/grey question mark ornament) - tests/unit/icons.test.ts: Updated 2 auditIcon assertions to expect \u{2754} - packages/tui/extensions/Worklog/terminal-utils.test.ts: Updated visible-width test to use the new character --- packages/tui/extensions/Worklog/terminal-utils.test.ts | 2 +- src/icons.ts | 2 +- tests/unit/icons.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/tui/extensions/Worklog/terminal-utils.test.ts b/packages/tui/extensions/Worklog/terminal-utils.test.ts index 35702d3a..269fb38b 100644 --- a/packages/tui/extensions/Worklog/terminal-utils.test.ts +++ b/packages/tui/extensions/Worklog/terminal-utils.test.ts @@ -170,7 +170,7 @@ describe('terminal-utils', () => { it('handles mixed icons with VS16 sequences correctly', () => { // 🔓 (2) + space (1) + 🛠️ (2) + space (1) + ❓ (2) = 8 - expect(visibleWidth('\u{1F513} \u{1F6E0}\uFE0F \u{2753}')).toBe(8); + expect(visibleWidth('\u{1F513} \u{1F6E0}\uFE0F \u{2754}')).toBe(8); }); it('treats ZWJ sequences correctly counting only visible characters', () => { diff --git a/src/icons.ts b/src/icons.ts index 010dcdc2..6edc557a 100644 --- a/src/icons.ts +++ b/src/icons.ts @@ -287,7 +287,7 @@ function auditKey(result: boolean | null | undefined): string { const AUDIT_ICON: Record = { yes: '\u{2705}', // ✅ no: '\u{274C}', // ❌ - unknown: '\u{2753}', // ❓ + unknown: '\u{2754}', // ❔ }; const AUDIT_FALLBACK: Record = { diff --git a/tests/unit/icons.test.ts b/tests/unit/icons.test.ts index ceefa52a..ca42bcc9 100644 --- a/tests/unit/icons.test.ts +++ b/tests/unit/icons.test.ts @@ -443,11 +443,11 @@ describe('auditIcon', () => { }); it('returns emoji for unknown (null)', () => { - expect(auditIcon(null)).toBe('\u{2753}'); // ❓ + expect(auditIcon(null)).toBe('\u{2754}'); // ❔ }); it('returns emoji for unknown (undefined)', () => { - expect(auditIcon(undefined)).toBe('\u{2753}'); // ❓ + expect(auditIcon(undefined)).toBe('\u{2754}'); // ❔ }); describe('with noIcons option', () => { From f7e750ce4bf331c5485f745ef74b50d18e5c083a Mon Sep 17 00:00:00 2001 From: Sorra the Orc <> Date: Wed, 8 Jul 2026 23:55:30 +0100 Subject: [PATCH 7/8] Bump version to v1.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d9941ac9..81db6969 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "worklog", - "version": "1.0.1", + "version": "1.0.2", "description": "A simple experimental issue tracker for AI agents", "main": "dist/index.js", "type": "module", From bf8a1c9bfbbfd197523930eca4b956ade1f224d4 Mon Sep 17 00:00:00 2001 From: Sorra the Orc <> Date: Wed, 8 Jul 2026 23:55:34 +0100 Subject: [PATCH 8/8] Update CHANGELOG.md for v1.0.2 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ecb771f6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## v1.0.2 (2026-07-08) +### Other +- Integrate model-display extension into the Worklog extension (WL-0MRC3F30E008XJN3) +- Move provider/model display to start of Worklog extension status line (WL-0MRC91VIN0089UXD) +- Change audit/review question mark icon from red to grey in selection list (WL-0MRCN2HF5003HSG9) +- Model display extension is not showing the provider (WL-0MRC3NOVZ000P0P5) +