Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e3370ba
feat(chat): read native Copilot CLI transcripts
YuuuXie Oct 21, 2026
398b531
fix(chat): recognize new Copilot sessions before the first message
Oct 22, 2026
fcbc616
fix(chat): recognize unwritten Copilot sessions started in terminals
Oct 22, 2026
eda8a07
fix(tests): preserve updated upstream browser lane expectations
Oct 22, 2026
4bd6345
test(chat): isolate Copilot browser fixture configuration
Oct 22, 2026
8548fd3
fix(chat): address Copilot transcript review findings
Oct 22, 2026
9a29f95
chore(chat): Merge current upstream main
YuuuXie Oct 22, 2026
fc1902d
fix(chat): Follow verified Copilot foreground sessions and render com…
YuuuXie Oct 22, 2026
fd6050b
fix(copilot): Invalidate switchback history and tool references
YuuuXie Oct 22, 2026
f806bd7
test(copilot): Confirm foreground re-registration after unregister
YuuuXie Oct 22, 2026
e23a5e5
test(copilot): Allow isolated lifecycle fixture to finish
YuuuXie Oct 22, 2026
fc3ca00
fix(chat): recover Copilot foreground log checkpoints safely
Oct 22, 2026
2bc5dab
fix(copilot): Run sandboxed browser fixture with Ubuntu Chrome
YuuuXie Oct 23, 2026
7d6cab5
chore(chat): Integrate current upstream main for CI
YuuuXie Oct 23, 2026
a9a558a
Merge latest upstream main into native Copilot Chat
Oct 23, 2026
260cfc0
Fix Copilot report fallback for logs without lifecycle events
Oct 23, 2026
6e89b4b
Recover Copilot lifecycle checkpoints and deduplicate completion summ…
Oct 24, 2026
919e169
Merge upstream main and port Copilot to extracted conversation modules
Oct 24, 2026
1f200f5
Retain invalid Copilot log evidence for the verified process lifetime
Oct 24, 2026
dab6d40
Keep native Copilot reader independent of launcher bookkeeping
Oct 24, 2026
1f5e47d
Merge synchronized upstream main into native Copilot Chat
Oct 24, 2026
6573986
fix(chat): retry unavailable conversations without changing lenses
Oct 25, 2026
bee51d6
refactor(copilot): share per-pass ownership evidence and test fixtures
Oct 25, 2026
be21457
chore: synchronize Copilot Chat with upstream 0.4.15
Oct 25, 2026
154d864
chore: synchronize Chat after upstream Copilot launcher merge
Oct 25, 2026
038ded8
docs(copilot): update the upstream agent row without duplicating it
Oct 25, 2026
b9ee361
chore: merge main into Copilot native chat
Oct 25, 2026
4cef1e3
fix(copilot): validate history and reuse request ownership evidence
devswha Oct 25, 2026
6851a0e
fix(chat): scope Copilot recovery polling and align browser checks
devswha Oct 25, 2026
0a763cf
test(copilot): add redacted recorded session fixture
Oct 25, 2026
c465ae2
fix(copilot): validate tool requests and bound log verification
devswha Oct 25, 2026
0d50bec
fix(copilot): recheck racily clean lifecycle logs
devswha Oct 25, 2026
7121af7
fix(copilot): widen timestamp margin and recheck clock rollback
devswha Oct 25, 2026
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
5 changes: 5 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1876,6 +1876,11 @@ Alert rows of the table above from the `--droplet-*` roles in `src/styles.css`.
that has not written its transcript yet (a new Claude Code or Codex pane before its first
message), so those are not greeted; an omo session not yet written answers an empty
conversation and is.
While terminal output stands in for an unavailable native conversation, Chat retries on the
existing 2 s polling cadence, not the 10 s native-push backstop. A verified native read restores
the ordinary cadence without a lens switch; rejected history stays hidden meanwhile. This
follows T3 Code's separation of [transport health and data freshness](https://github.com/pingdotgg/t3code/blob/main/docs/internals/connection-runtime.md):
an open socket alone does not prove the conversation is current. No code or new controls are borrowed.

### Work block (`.work-block`, `.work-row`)
- One per assistant turn: a `▸ Worked for 7s · 1 edit · 2 commands` header (duration = next turn's
Expand Down
7 changes: 7 additions & 0 deletions changelog.d/copilot-native-chat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Added
- Copilot CLI conversations can show native text, tool activity, recorded model, question choices
and Autopilot completion summaries in chat on Linux when the foreground session is verified.
Verified new sessions can show an empty chat before the first message; resumed sessions
follow process-owned foreground lifecycle records with bounded, revalidated log reads.
A trusted Herdr report and unique held marker can identify history only without conflicting
lifecycle evidence. Unverifiable sessions use Terminal; interactive answers stay there too.
85 changes: 85 additions & 0 deletions docs/copilot-chat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copilot chat

On Linux, Chat reads Copilot CLI's native `events.jsonl`: user and assistant text,
tool activity, recorded model/effort, recorded `ask_user` choices and Autopilot completion
summaries. Reading Chat makes no model calls. Live questions and approvals stay in Terminal;
the bridge never sends guessed selection keys. macOS and Windows use Terminal.
This reader supports existing panes; it does not add Copilot to the launch picker.

## Setup and session identity

Run `herdr integration install copilot` in the effective Copilot home before starting a
pane to enable Herdr's session report. Ovis never changes the CLI configuration.
The reader uses the foreground process's `--config-dir`, then `COPILOT_HOME`, otherwise
`HOME/.copilot`; an explicit `--log-dir` selects its log directory.

The foreground process's own open CLI log can identify its current session through
`Registering foreground session` and `Unregistering foreground session` records.
Copilot CLI 1.0.103 was observed emitting these records; this is not a guarantee for every
CLI version. A fresh registration can follow `/resume`, `/fork` and switch-back even
when older sessions remain open. File recency never selects a session.

Without a selected lifecycle registration, reading history requires both a trusted Herdr
report and exactly one canonical session marker held by that foreground process.
This fallback is allowed when no process log has been observed, or when the complete initial
log contains no lifecycle events. It does **not** recover from an unregister, ambiguous
lifecycle records, incomplete log lines, lost/rewritten log continuity, a deleted or unreadable
log, or a catch-up gap beyond the 16 MiB scan window without a fresh registration.
These cases use Terminal until current foreground evidence is available; repeated polling
does not turn invalid evidence into permission to read old history.

Existing panes with neither proof cannot be recovered by restarting the web server.
Resume the intended session in the CLI to establish fresh foreground evidence.

## History and limits

Before its first message, a genuinely new process can show an empty Chat when its
process-owned log or unique held marker identifies an unwritten session.
An explicit resume/continue without current foreground proof never gets a
fresh-session placeholder.

A recorded foreground switch invalidates pagination cursors and tool-output links,
including after switching back to the same session. Reload the current page for fresh
links. The marker-and-report fallback cannot detect an unobserved switch away and back.

The reader scans at most 16 MiB of lifecycle-log catch-up per lookup and checkpoints
complete reads, including unavailable selections. Ambiguity remains unavailable until a
fresh registration; it does not permanently block later registrations.
Unchanged file identity, size and timestamps reuse the last verification only when both
mtime and ctime were at least three seconds old at that check and the wall clock has not
moved behind it; younger timestamps or a clock rollback trigger rehashing
because a same-size rewrite may share their filesystem tick. Changed logs
revalidate the entire consumed prefix in 64 KiB chunks, including bytes outside the catch-up
window; logs over 64 MiB use Terminal without reading or hashing them.
This assumes filesystem timestamps trail this machine's clock by no more than that margin.
A filesystem with greater timestamp lag, such as a skewed network mount, can retain a stale
binding until the next append.
An incomplete trailing line keeps the last complete
binding only when its prefix rules out a lifecycle event. A partial timestamp or possible
foreground switch temporarily blocks Chat until the line is complete.
Observation state is bounded to 256 live process identities. Exited or replaced processes
release slots; at capacity, untracked processes use Terminal rather than forgetting a live
process's invalid evidence.
Transcript pages use the shared bounded pager. After reading, the pane's own process is
checked again using the request's existing peer claims. Unsupported or malformed complete
message records use Terminal instead of an empty Chat.
Tool-output searches read at most 32 MiB; an output beyond that limit is unavailable.

## Verification

```sh
HERDR_TEST_MODE=unit bun test server/copilot.test.ts
bun run check run bun test --timeout 20000 server/copilot.contract.test.ts
bun run check run --build bun scripts/copilot-chat-browser-qa.ts
bun run check fast
```

The HTTP contract and browser fixtures use fictional transcripts and disposable processes.
`server/fixtures/copilot-recorded.jsonl` is a redacted seven-record excerpt from a real CLI
session whose header identifies Copilot 1.0.102. Field names, nested collections, scalar types,
empty values and record/phase discriminators are preserved. Text, paths, identifiers, models,
timestamps and telemetry values are replaced; intervening records are omitted, so some parent
IDs point outside the excerpt. The unit test covers a root prompt with task ancestry, a
tool-only assistant message, its request/result and recorded assistant text. It does not
establish 1.0.103 lifecycle ordering, universal marker holding, a subagent discriminator or
compatibility with every CLI release. No raw private transcript or process log is published.
3 changes: 2 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ the test runner. A report is evidence, not an authorization to merge or publish.

A local pass is not CI's: the PC has its own Node (CI pins 22), its own cores and its own system
libraries. The browser lane uses the lockfile's Chromium, which it downloads into Playwright's
cache on first use. `HERDR_TEST_SHARDS=4` runs four integration files at a time, each with an
cache on first use.
`HERDR_TEST_SHARDS=4` runs four integration files at a time, each with an
isolated herdr session. Local checks and hosted CI default to one; self-hosted CI uses four.

The single commands still work on their own:
Expand Down
2 changes: 1 addition & 1 deletion docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ Every agent herdr runs shows up with its live status, terminal and alerts. The c
| **Hermes** | Native SQLite session store | — use Terminal |
| **Antigravity (agy)** | **Experimental; macOS untested.** Native ledger, selected by the running process’s held conversation lock. Linux native-tested; Windows uses terminal text. See [Agy behavior and limits](agy-chat.md). | — use Terminal |
| **Letta Code** | The terminal's text. Choose **Letta Code** in **New workspace**, **New tab with agent…** or **New worktree** where `letta` is installed, or run `letta` in a herdr pane's **Terminal**. | — use Terminal |
| **GitHub Copilot CLI** | The terminal's text. Choose **GitHub Copilot** in **New workspace**, **New tab with agent…** or **New worktree** where `copilot` is installed, or run `copilot` in a herdr pane's **Terminal**. | — use Terminal |
| **GitHub Copilot CLI** | Linux native transcript when the foreground session is verified. See [Copilot setup and limits](copilot-chat.md). | — recorded choices are read-only; answer questions and approvals in Terminal |
| **Anything else** | The terminal's text | — use Terminal |

GJC chat and GJC done alerts use a native extension rather than SDK session queries. From
Expand Down
31 changes: 31 additions & 0 deletions scripts/chat-fallback-browser-qa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CONTENT_SECURITY_POLICY } from "../server/static.ts";
import { chromePath } from "./chrome-path.ts";
import { parseClaudeTranscript } from "../server/conversation.ts";
import type { ConversationTurn } from "../shared/protocol.ts";
import type {} from "./chat-fallback-fixture.tsx";
const root = realpathSync(mkdtempSync(join(tmpdir(), "herdr-chat-fallback-")));
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined;
let server: ReturnType<typeof Bun.serve> | undefined;
Expand Down Expand Up @@ -171,6 +172,36 @@ try {
console.log("PASS Claude hook narration and replies stay visible outside folded work; the tool remains inside");
}
assert.deepEqual(errors, []);
structured = false;
const polling = await browser.newPage();
let pollingReads = 0;
polling.on("request", (request) => {
if (new URL(request.url()).pathname === "/api/pane/conversation") pollingReads += 1;
});
await polling.clock.install();
await polling.clock.pauseAt(new Date());
await polling.addInitScript(() => {
localStorage.setItem("ovis:settings", JSON.stringify({ language: "en" }));
});
await polling.goto(`http://127.0.0.1:${server.port}/?pushes`);
await polling.locator(".chat-terminal-fallback summary").waitFor();
const codexReads = pollingReads;
await polling.clock.runFor(2000);
assert.equal(pollingReads, codexReads, "non-Copilot terminal fallback keeps its 10s backstop");
const codexBackstop = polling.waitForResponse((response) => new URL(response.url()).pathname === "/api/pane/conversation");
await polling.clock.runFor(8000);
await codexBackstop;
assert.equal(pollingReads, codexReads + 1);
const changedAgent = polling.waitForResponse((response) => new URL(response.url()).pathname === "/api/pane/conversation");
await polling.evaluate(() => window.chatFallbackQA.agent("copilot"));
await changedAgent;
const copilotReads = pollingReads;
const copilotPoll = polling.waitForResponse((response) => new URL(response.url()).pathname === "/api/pane/conversation");
await polling.clock.runFor(2000);
await copilotPoll;
assert.equal(pollingReads, copilotReads + 1, "only Copilot terminal fallback uses the 2s recovery poll");
await polling.close();
console.log("PASS fallback polling keeps 10s for other agents and 2s for Copilot under a controlled clock");
console.log("chat fallback: browser OK (no hidden terminal reads, opened stalled output cannot block structured refresh, late cleanup, non-agent inline output, fallback error announcement and accurate empty state)");

} finally {
Expand Down
2 changes: 1 addition & 1 deletion scripts/chat-fallback-fixture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ function Fixture() {
const [pane, setPane] = useState("agent");
const [refresh, setRefresh] = useState(0);
window.chatFallbackQA = { pane: setPane, agent: setAgent, refresh: () => setRefresh((key) => key + 1) };
return <main style={{ height: "100vh" }}><ChatView key={pane} paneId={pane} refreshKey={refresh} connected ended={false} agent={pane === "shell" ? null : agent} /></main>;
return <main style={{ height: "100vh" }}><ChatView key={pane} paneId={pane} refreshKey={refresh} connected ended={false} agent={pane === "shell" ? null : agent} conversationPushes={new URLSearchParams(location.search).has("pushes")} /></main>;
}
createRoot(document.getElementById("root")!).render(<React.StrictMode><SettingsProvider><Fixture /></SettingsProvider></React.StrictMode>);
1 change: 1 addition & 0 deletions scripts/ci-browser.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ run_script scripts/chat-scroll-padding-regression.ts
run_script scripts/chat-selection-copy-regression.ts
run_script scripts/chat-fallback-browser-qa.ts
run_script scripts/grok-chat-browser-qa.ts
run_script scripts/copilot-chat-browser-qa.ts
run_script scripts/omo-chat-browser-qa.ts
run_script scripts/omo-plan-browser-regression.ts
run_script scripts/plan-chip-demo-regression.ts
Expand Down
1 change: 1 addition & 0 deletions scripts/ci-browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ describe("browser lane", () => {
const { text, code, evidence } = await lane();
expect(code).toBe(0);
expect(rows(text).map((row) => row.script)).toEqual(["scripts/demo-build.ts", ...serialBrowserScripts()]);
expect(rows(text).map((row) => row.script)).toContain("scripts/copilot-chat-browser-qa.ts");
expect(rows(text).every((row) => row.code === 0 && row.seconds >= 0)).toBe(true);
expect(evidence).toBe(false);
expect(text).not.toMatch(/EVIDENCE=.+/);
Expand Down
Loading
Loading