Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
"No GitHub Copilot sign-in wall for chat"
],
"proof_command": "manual signed-in /IDE session smoke + ACP prompt receipt",
"evidence_note": "Workspace deploy a0f49841 SUCCESS (81a3ada1 / PR #196). Live /opt/commonplace/extensions/theorem-vscode has theorem.openChat + theorem.chat in dist bundles; studio-server extensions list theorem-vscode only (no copilot)."
"evidence_note": "Workspace deploy a0f49841 SUCCESS (81a3ada1 / PR #196). Live pack theorem.openChat + theorem.chat. Spec-review Pass B: ACP agent_message_chunk \u2192 onDelta; Pass C: openChat hidden unless THEOREM_ACP_WS_URL/theorem.agentUrl. Interactive signed-in panel one-turn still optional human smoke."
},
{
"id": "CR-006",
Expand Down
1 change: 1 addition & 0 deletions apps/theorem-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
],
"menus": {
"commandPalette": [
{ "command": "theorem.openChat", "when": "theorem.acpConfigured" },
{ "command": "theorem.applyFix", "when": "false" },
{ "command": "theorem.diffRevision", "when": "false" },
{ "command": "theorem.restoreRevision", "when": "false" },
Expand Down
17 changes: 17 additions & 0 deletions apps/theorem-vscode/src/agent/acp-chunks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SOURCING: @commonplace/theorem-acp/state. Pure filter for Studio chat deltas.
import { agentMessageTextFromUpdate, type AcpSessionUpdate } from '@commonplace/theorem-acp/state';

/**
* Forward agent_message_chunk text for the active session into a ChatTransport
* onDelta callback. Ignores other update kinds and other sessions.
*/
export function forwardAgentMessageChunk(
sessionId: string,
notification: { readonly sessionId: string; readonly update: AcpSessionUpdate },
onDelta: (chunk: string) => void,
): void {
if (notification.sessionId !== sessionId) return;
if (notification.update.sessionUpdate !== 'agent_message_chunk') return;
const chunk = agentMessageTextFromUpdate(notification.update);
if (chunk) onDelta(chunk);
}
3 changes: 1 addition & 2 deletions apps/theorem-vscode/src/agent/chat-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@ export class TheoremChatPanel {
},
prompt: async (_sessionId, text, onDelta) => {
if (!session) throw new Error('theorem.chat: no ACP session');
await session.prompt(text);
onDelta('Turn submitted over Theorem ACP.');
await session.prompt(text, onDelta);
},
dispose: () => {
session?.dispose();
Expand Down
6 changes: 5 additions & 1 deletion apps/theorem-vscode/src/agent/presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export interface PresenceConfig {
/** The subset of the ACP session surface the IDE end drives. */
export interface AcpSession {
readonly sessionId: string;
prompt(text: string): Promise<void>;
/**
* Send one user turn. When `onDelta` is provided, agent_message_chunk text
* from session/update notifications is forwarded as it arrives.
*/
prompt(text: string, onDelta?: (chunk: string) => void): Promise<void>;
onPermissionRequest(handler: (request: PermissionRequest) => Promise<PermissionOutcome>): void;
dispose(): void;
}
Expand Down
14 changes: 12 additions & 2 deletions apps/theorem-vscode/src/agent/session-opener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import type { TheoremPackConfig } from '../config';
import type { AcpSession, PermissionOutcome, PermissionRequest } from './presence';
import { forwardAgentMessageChunk } from './acp-chunks';

/**
* Open the window's session.
Expand Down Expand Up @@ -47,8 +48,17 @@ export async function openIdeSession(

return {
sessionId,
prompt: async (text) => {
await client.prompt(sessionId, text);
prompt: async (text, onDelta) => {
const unsub = onDelta
? client.onSessionUpdate((notification) => {
forwardAgentMessageChunk(sessionId, notification, onDelta);
})
: undefined;
try {
await client.prompt(sessionId, text);
} finally {
unsub?.();
}
},
onPermissionRequest: (handler) => {
client.onRequestPermission(async (request) => {
Expand Down
14 changes: 11 additions & 3 deletions apps/theorem-vscode/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export interface TheoremPackConfig {
readonly consoleOrigin: string;
readonly agentUrl: string;
readonly token?: string;
/**
* True when THEOREM_ACP_WS_URL or theorem.agentUrl is set. SPEC rollback:
* hide Theorem: Open Chat when ACP URL is unset (consoleOrigin alone is not
* an explicit ACP door).
*/
readonly acpConfigured: boolean;
}

function env(name: string): string | undefined {
Expand Down Expand Up @@ -47,10 +53,11 @@ export function resolveTheoremPackConfig(config: WorkspaceConfiguration): Theore
?? config.get<string>('consoleOrigin')
?? 'https://v2.theoremharness.com';

const agentUrl =
const explicitAgentUrl =
env('THEOREM_ACP_WS_URL')
?? config.get<string>('agentUrl')
?? consoleOrigin;
?? (config.get<string>('agentUrl')?.trim() || undefined);
const acpConfigured = Boolean(explicitAgentUrl);
const agentUrl = explicitAgentUrl ?? consoleOrigin;

const token =
env('THEOREM_EDITOR_API_KEY')
Expand All @@ -63,6 +70,7 @@ export function resolveTheoremPackConfig(config: WorkspaceConfiguration): Theore
...(projectId ? { projectId } : {}),
consoleOrigin,
agentUrl,
acpConfigured,
...(token ? { token } : {}),
};
}
14 changes: 13 additions & 1 deletion apps/theorem-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export function activate(context: vscode.ExtensionContext): void {
const output = vscode.window.createOutputChannel('Theorem');
const config = vscode.workspace.getConfiguration('theorem');
const resolved = resolveTheoremPackConfig(config);
void vscode.commands.executeCommand('setContext', 'theorem.acpConfigured', resolved.acpConfigured);
if (!resolved.acpConfigured) {
output.appendLine('chat: ACP URL unset (set THEOREM_ACP_WS_URL or theorem.agentUrl); Theorem: Open Chat hidden');
}

const client = new SubstrateClient({
endpoint: {
Expand Down Expand Up @@ -194,7 +198,15 @@ export function activate(context: vscode.ExtensionContext): void {
},
),
vscode.commands.registerCommand('theorem.startSession', () => presence.start()),
vscode.commands.registerCommand('theorem.openChat', () => chatPanel.show()),
vscode.commands.registerCommand('theorem.openChat', () => {
if (!resolved.acpConfigured) {
void vscode.window.showWarningMessage(
'Theorem Chat needs THEOREM_ACP_WS_URL or theorem.agentUrl before opening.',
);
return;
}
chatPanel.show();
}),
vscode.commands.registerCommand('theorem.showHistory', async (target?: vscode.Uri) => {
const uri = target ?? vscode.window.activeTextEditor?.document.uri;
if (uri) await showHistoryQuickPick(timeline, uri);
Expand Down
84 changes: 84 additions & 0 deletions apps/theorem-vscode/test/acp-chat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SOURCING: none. Spec-review completion Pass B/C — ACP chunk forward + acpConfigured.
import { describe, expect, it, vi } from 'vitest';
import { forwardAgentMessageChunk } from '../src/agent/acp-chunks';
import { resolveTheoremPackConfig } from '../src/config';

function fakeConfig(values: Record<string, string | undefined>) {
return {
get<T>(key: string): T | undefined {
return values[key] as T | undefined;
},
};
}

describe('forwardAgentMessageChunk', () => {
it('forwards agent_message_chunk text for the active session', () => {
const onDelta = vi.fn();
forwardAgentMessageChunk(
'sess-1',
{
sessionId: 'sess-1',
update: {
sessionUpdate: 'agent_message_chunk',
content: { type: 'text', text: 'hello' },
},
},
onDelta,
);
expect(onDelta).toHaveBeenCalledWith('hello');
});

it('ignores other sessions and update kinds', () => {
const onDelta = vi.fn();
forwardAgentMessageChunk(
'sess-1',
{
sessionId: 'other',
update: {
sessionUpdate: 'agent_message_chunk',
content: { type: 'text', text: 'nope' },
},
},
onDelta,
);
forwardAgentMessageChunk(
'sess-1',
{
sessionId: 'sess-1',
update: { sessionUpdate: 'theorem_turn_activity', status: 'running' },
},
onDelta,
);
expect(onDelta).not.toHaveBeenCalled();
});
});

describe('acpConfigured', () => {
it('is false when ACP URL is unset', () => {
const prev = process.env.THEOREM_ACP_WS_URL;
delete process.env.THEOREM_ACP_WS_URL;
try {
const pack = resolveTheoremPackConfig(fakeConfig({}) as never);
expect(pack.acpConfigured).toBe(false);
expect(pack.agentUrl).toBe('https://v2.theoremharness.com');
} finally {
if (prev === undefined) delete process.env.THEOREM_ACP_WS_URL;
else process.env.THEOREM_ACP_WS_URL = prev;
}
});

it('is true when theorem.agentUrl is set', () => {
const prev = process.env.THEOREM_ACP_WS_URL;
delete process.env.THEOREM_ACP_WS_URL;
try {
const pack = resolveTheoremPackConfig(
fakeConfig({ agentUrl: 'wss://example/v1/commonplace/acp/ws' }) as never,
);
expect(pack.acpConfigured).toBe(true);
expect(pack.agentUrl).toContain('acp/ws');
} finally {
if (prev === undefined) delete process.env.THEOREM_ACP_WS_URL;
else process.env.THEOREM_ACP_WS_URL = prev;
}
});
});
43 changes: 15 additions & 28 deletions docs/plans/theorem-chat-register/EXECUTE-REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,32 @@
Plan: `plan-theorem-chat-register-20260805a`

## Summary
- Final condition: Copilot retired; sticky `/workspace/repo` repaired; Theorem chat register package mounted at console `/chat` and in the live Studio pack as `Theorem: Open Chat`.
- Goal achieved: **yes** for CR-001..007 (live evidence for CR-005 is pack command + `theorem.chat` in seeded extension bundles on deploy `a0f49841`; interactive signed-in panel click still a human smoke if desired).
- Biggest remaining risk: rotate leaked Git/workspace tokens from prior Studio build/SSH surfaces; prefer GitHub-triggered deploys over `railway up` from repo root.
- Next action: optional signed-in `/IDE` → Command Palette → Theorem: Open Chat; archive OpenWork leftovers after `2026-09-01` (D2).
- Final condition: Copilot retired; sticky `/workspace/repo` repaired; Theorem chat register package mounted at console `/chat` and in the live Studio pack as `Theorem: Open Chat`. Spec-review completion pass closed Studio ACP streaming stub, ACP-URL command hide, and doc/health residual honesty.
- Goal achieved: **yes** for CR-001..007 product cutover; interactive signed-in `/IDE` Open Chat + authenticated `/chat` HTML stamp remain the strongest remaining live smokes (doctor accepts loginish for `/chat`).
- Biggest remaining risk: rotate leaked Git/workspace tokens from prior Studio build/SSH surfaces; OpenWork `:8787` still owns container HEALTHCHECK (not product `/chat`).
- Next action: optional signed-in `/IDE` → Theorem: Open Chat one-turn; signed-in `/chat` stamp screenshot.

## Checklist Reconciliation
| ID | Task | Status | Evidence | Validation | Notes |
|---|---|---|---|---|---|
| CR-000 | Plan board | done | PLAN + checklist | artifact | |
| CR-001 | Copilot retirement | done | PR #190/#191; deploy `4e69fb85` | live | |
| CR-002 | Volume repair | done | package.json + HEAD on volume | live | |
| CR-003 | Register contract | done | SPEC-THEOREM-CHAT-REGISTER-1.0 | artifact | |
| CR-003 | Register contract | done | SPEC-THEOREM-CHAT-REGISTER-1.0 | artifact | ChatTransport shape amended to shipped API |
| CR-004 | Package | done | `@commonplace/theorem-chat-register` | 3/3 tests | |
| CR-005 | Studio mount | done | PR #196; deploy `a0f49841` SUCCESS | live pack `theorem.openChat` + `theorem.chat` in dist | prior `1537b625` FAILED missing COPY |
| CR-006 | `/chat` + OpenWork out | done | console `a4ae048d`; doctor 26/26 | live | rollback `CONSOLE_OPENWORK_CHAT_PROXY=1` |
| CR-007 | Debt + report | done | this file + supersession | artifact | D2 deferred |
| CR-005 | Studio mount | done | PR #196; deploy `a0f49841`; ACP chunk forward + `acpConfigured` hide | pack + unit tests | interactive panel smoke optional |
| CR-006 | `/chat` + OpenWork out | done | console `a4ae048d`; doctor 26/26 | live | auth HTML stamp optional |
| CR-007 | Debt + report | done | this file + supersession | artifact | `:8787` health residual recorded |

## Changes Made (final execute pass)
| Area | Files | Summary | Why |
|---|---|---|---|
| Workspace pack | `packaging/workspace/Dockerfile` | COPY `packages/theorem-chat-register` in `vscode-pack` | fix esbuild resolve |
| Watch | `packaging/workspace/railway.toml` | watch package path | rebuild on register edits |
| PRs | #196 merged; #195 closed | clean main-based fix | avoid add/add conflicts |

## Validation
| Check | Result | Notes |
## Spec-review completion (Passes A–D)
| Pass | Change | Proof |
|---|---|---|
| `pnpm --filter @commonplace/theorem-chat-register test` | pass | 3 tests |
| `pnpm --filter theorem-vscode build` | pass | local |
| `node scripts/doctor.mjs` | pass | 26/26 |
| Live console `/chat` | pass | no `openwork.chat` host |
| Live workspace deploy | pass | `a0f49841` SUCCESS @ `81a3ada1` |
| Live pack Open Chat | pass | `/opt/commonplace/extensions/theorem-vscode` contributes `theorem.openChat`; dist mentions `theorem.chat` ×9 |

## Remaining Work
- What remains: optional interactive `/IDE` palette smoke; token rotation; D2 archive pass later.
- Why: pack/command contribution is the deploy oracle; UI click needs a signed-in browser session.
- Next step: rotate secrets; human smoke if wanted.
| B | Studio `session.prompt(..., onDelta)` forwards `agent_message_chunk` | `acp-chunks.ts` + `acp-chat.test.ts`; theorem-acp `agentMessageTextFromUpdate` |
| C | Hide `theorem.openChat` unless `THEOREM_ACP_WS_URL` / `theorem.agentUrl` | `acpConfigured` + package.json `when` |
| D | SPEC ChatTransport amend; PLAN CR-006 done; Dockerfile health comment; this report | docs |
| A | Live unauth `/chat` 307 login (no OpenWork proxy); pack `openChat` present | signed-in UI still human |

## Rollback
- `CONSOLE_OPENWORK_CHAT_PROXY=1` restores `/chat` → workspace `:8787` OpenWork proxy.
- Do not restore Copilot `defaultChatAgent` keys.
- Workspace HEALTHCHECK remains on OpenWork `:8787` until a follow-up moves it.
2 changes: 1 addition & 1 deletion docs/plans/theorem-chat-register/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Authenticated users get:
| CR-003 | Register contract: package path, `register_impl`, dual seams, retirement inventory | SPEC draft under this plan | named ids + non-goal on `IDefaultChatAgent` | **done** |
| CR-004 | Implement Theorem chat register package | new package + pack/console adapters | package tests; no openworklabs/opencode on happy path | **done** |
| CR-005 | Mount register in Studio agent/chat panel | theorem-vscode + Studio product settings | signed-in `/IDE` shows Theorem register; no Copilot sign-in wall | **done** (pack); live smoke after seed |
| CR-006 | Mount register at `/chat`; retire OpenWork door | `OpenworkChatRegister`, middleware, `.commonplace-canonical`, workspace image | stamp ≠ `openwork.chat`; doctor + register-manifest | **verifying** (await console deploy) |
| CR-006 | Mount register at `/chat`; retire OpenWork door | `OpenworkChatRegister`, middleware, `.commonplace-canonical`, workspace image | stamp ≠ `openwork.chat`; doctor + register-manifest | **done** |
| CR-007 | Retire residual LLM / OpenWork surface debt + EXECUTE-REPORT | docs + deletions | EXECUTE-REPORT; no product route serves OpenWork as chat | **done** |

## Sequence
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ must become **one** Theorem chat register. Two hosts, one package, one
### Public API (CR-004)

- `REGISTER_IMPL = 'theorem.chat'`
- `ChatTransport`: `{ openSession(), prompt(text), subscribe(listener), dispose() }`
- `ChatTransport`: `{ openSession(), prompt(sessionId, text, onDelta), dispose() }`
- `onDelta` receives assistant text chunks as they arrive
- Snapshot `subscribe` lives on `createChatSessionController`, not on the transport
- `createChatSessionController(transport)`: session open + one-turn prompt; pure logic, no React
- `TheoremChatRegister`: React shell with `data-register-impl="theorem.chat"`, composer, message list

Expand Down Expand Up @@ -66,4 +68,5 @@ until the pack implements VS Code Chat participant / host APIs. Overlay deletion
## 7. Rollback

- Re-enable OpenWork middleware behind an explicit env (`CONSOLE_OPENWORK_CHAT_PROXY=1`) only as emergency rollback; default is Theorem register.
- Studio pack: hide Theorem Chat view command if ACP URL unset; do not restore Copilot product keys.
- Studio pack: hide Theorem Chat (`theorem.openChat`) from the command palette when ACP URL is unset (`THEOREM_ACP_WS_URL` / `theorem.agentUrl`); do not restore Copilot product keys.
- Workspace healthcheck may remain on OpenWork `:8787` until a follow-up moves health to the Studio IDE door; that residual is not the product `/chat` host.
15 changes: 15 additions & 0 deletions packages/theorem-acp/src/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from './hosted-client.js';
import {
TURN_CONTEXT_SCHEMA,
agentMessageTextFromUpdate,
applySessionUpdate,
beginTurn,
cancelTurn,
Expand Down Expand Up @@ -200,3 +201,17 @@ test('refusal remains terminal when a later completion is delivered', () => {

assert.equal(replayed.turnStatus, 'refused');
});

test('agentMessageTextFromUpdate reads canonical ACP text chunks', () => {
assert.equal(
agentMessageTextFromUpdate({
sessionUpdate: 'agent_message_chunk',
content: { type: 'text', text: 'streamed' },
}),
'streamed',
);
assert.equal(
agentMessageTextFromUpdate({ sessionUpdate: 'theorem_turn_activity', status: 'running' }),
'',
);
});
5 changes: 5 additions & 0 deletions packages/theorem-acp/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,11 @@ function isSettled(status: TheoremAgentState['turnStatus']): boolean {
return status !== 'idle' && status !== 'running';
}

/** Extract assistant text from an ACP session update (agent_message_chunk). */
export function agentMessageTextFromUpdate(update: AcpSessionUpdate): string {
return readText(update);
}

function readText(update: AcpSessionUpdate): string {
const content = update.content as
| { type?: string; text?: string; content?: { type?: string; text?: string } }
Expand Down
5 changes: 3 additions & 2 deletions packaging/workspace/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,9 @@ RUN mkdir -p "${WORKSPACE_DIR}" "${XDG_DATA_HOME}" "${XDG_CONFIG_HOME}" "${OPENW

EXPOSE 8787 8080 50090

# The chat door is the healthcheck: it is the one the console proxies to, so
# an image that answers here is an image the console can actually use.
# The OpenWork `:8787` process remains the container HEALTHCHECK target until a
# follow-up moves health onto the Studio IDE door. Product `/chat` is Theorem
# register on the console (not this proxy). See SPEC-THEOREM-CHAT-REGISTER-1.0.
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=5 \
CMD curl -fsS "http://127.0.0.1:${OPENWORK_PORT}/health" || exit 1

Expand Down
Loading