Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7125193
shell: History, every chat the person has had, and its search
futurepaul Oct 31, 2026
c03dcf2
shell: a message sent in an archived chat brings it back
futurepaul Oct 31, 2026
ce30865
shell: History lists what was said, each row with its time
futurepaul Oct 31, 2026
a9cc5b9
docs: History lists what was said, each row with its time
futurepaul Oct 31, 2026
8bba0a2
shell: an import opened again leaves Back to the view before
futurepaul Oct 31, 2026
5b6e722
shell: a history card holds its summary, then what was said after it
futurepaul Oct 31, 2026
f0c2a31
shell: Continue with an agent of your choosing, from an import or an …
futurepaul Oct 31, 2026
db8fae5
e2e: a continued chat's note says whose words they were
futurepaul Oct 31, 2026
909502e
xtask: the archive's Continue test makes it as the shell does now
futurepaul Oct 31, 2026
86ebeba
e2e: History, as the person uses it
futurepaul Oct 31, 2026
e664d51
Merge commit '6e373e95' into claude/history-view
futurepaul Oct 31, 2026
b79a2ee
shell: Continue on History: its rows' controls, and the e2e through H…
futurepaul Oct 31, 2026
6e8660c
Merge remote-tracking branch 'origin/claude/history-view' into claude…
futurepaul Oct 31, 2026
0e68307
shell: Continue drops the archive's old continued/<key>.json markers
futurepaul Oct 31, 2026
3820b13
Merge remote-tracking branch 'origin/master' into claude/history-view
futurepaul Oct 31, 2026
9fcd7fd
docs: the shell's row names History, where the Session Archive's chat…
futurepaul Oct 31, 2026
486c39b
shell: Continue's Open is a chat finished, never one cut short
futurepaul Oct 31, 2026
c3f33af
Merge remote-tracking branch 'origin/master' into claude/history-view
futurepaul Oct 31, 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
2 changes: 1 addition & 1 deletion SPECIAL-CASE-INVENTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ a vendor's product beyond the credential it holds.
| Surface | Why it is platform | Built on |
|---|---|---|
| Sign-in, sessions, CLI approval (`/auth/*`, `/cli`; the shell's tabs sign in through `/auth/frame`; a stranger's fragment asks first, at `/auth/fragment`) | Security: it mints the credentials every other surface trusts. Which of a browser's cookies count on a fragment's origin is the router's (Fetch Metadata), so another fragment's page reaches it as a stranger (docs/api.md, Sign-in). | WorkOS AuthKit; the registry |
| The shell (`/`, `/settings`, `/apps`, `/welcome`): sidebar, desktop (its dock and windows), settings, the Apps page (each of your fragments but chats and agents: your dock, its card, its icon, who has access and its share sheet; one page, as it acts as you over all of them, which no fragment may), the launcher (search), first run (the welcome); your Session Archive (your one fragment of kind `sessions`: its chats' index under a heading below your chats, each opened read-only in the middle column, its own page framed as a chat is; its Continue, asked by that page and done by the shell as you, makes a chat with your agent whose first record is the conversation's history: templates/sessions/README.md, docs/chat-records.md); to a page of a fragment you own that asks, your agents, and adding one of them to that fragment; to your own chat's page, a way to ask for an app put in its composer (`compose`; docs/api.md, The shell); to a chat's page of yours, its alerts, which no frame may ask the browser for (the shell's alerts worker, one subscription a chat, made with the chat's own key); what your agent is doing, which it reads from your chats' rows' listed records by the chat's turn kinds (docs/chat-records.md): the shell is that contract's third reader, beside the bridge and the chat's page, so a change to it changes the shell too | Security: it holds your session and frames your fragments and computers. The thin page users can't break. A page asks it, never grants: the shell checks the page is your own fragment's, at its own origin, offers only your own agents, and adds one only once you confirm it in the shell's own dialog, every time. A chat's page takes `compose` only from the platform's origin (its hello's `platform`) and sends nothing: you do. | The public fragment, computer, identity and ledger APIs |
| The shell (`/`, `/settings`, `/apps`, `/history`, `/welcome`): sidebar, desktop (its dock and windows), settings, History (every chat you have had, archived and imported ones too, and its search), the Apps page (each of your fragments but chats and agents: your dock, its card, its icon, who has access and its share sheet; one page, as it acts as you over all of them, which no fragment may), the launcher (search), first run (the welcome); your Session Archive (your one fragment of kind `sessions`: its chats' index listed in History, each opened read-only in the middle column, its own page framed as a chat is; its Continue, asked by that page and done by the shell as you, makes a chat with the agent of yours you pick (one a conversation and agent) whose first record is the conversation's history, and does the same for an archived chat of yours: templates/sessions/README.md, docs/chat-records.md); to a page of a fragment you own that asks, your agents, and adding one of them to that fragment; to your own chat's page, a way to ask for an app put in its composer (`compose`; docs/api.md, The shell); to a chat's page of yours, its alerts, which no frame may ask the browser for (the shell's alerts worker, one subscription a chat, made with the chat's own key); what your agent is doing, which it reads from your chats' rows' listed records by the chat's turn kinds (docs/chat-records.md): the shell is that contract's third reader, beside the bridge and the chat's page, so a change to it changes the shell too | Security: it holds your session and frames your fragments and computers. The thin page users can't break. A page asks it, never grants: the shell checks the page is your own fragment's, at its own origin, offers only your own agents, and adds one only once you confirm it in the shell's own dialog, every time. A chat's page takes `compose` only from the platform's origin (its hello's `platform`) and sends nothing: you do. | The public fragment, computer, identity and ledger APIs |
| Identity and delegation | Security: who an agent acts for, and at what role. | The registry |
| Optional public addresses (`GET`/`PUT /api/f/{name}/address`) | Security: first-come reservations coordinate across fragments and permanently bind an origin to one app identity, so a delete never gives another app its cookies or storage. | The registry and each fragment's own identity and owner |
| Connections and the egress swap | Security: it holds the route to your accounts' tokens and the operator's keys. | WorkOS Pipes; the computer's intercepts |
Expand Down
62 changes: 62 additions & 0 deletions cell/shell-tests/history-page.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// cell/shell/history-page.js's rows (Paul, 2026-10-31): every chat and
// imported conversation, the newest first; All, Archived and Imported;
// words held by a title (an import's first words too) or found by the
// person's search; the search's hint bounded as the API bounds it; and a
// row's time.
// Run by `cargo xtask check` on the pinned Node (xtask/src/js_syntax.rs).

import { test } from "node:test";
import assert from "node:assert/strict";
import { firstOf, historyRows, hintOf, when } from "../shell/history-page.js";

const chat = (name, at, extra = {}) => ({ name, title: name, at, archived: false, ...extra });
const entry = (key, ended_at, extra = {}) => ({ key, title: key, ended_at, preview: "", ...extra });
const keys = (rows) => rows.map((r) => r.key);
const chats = [chat("trip", 30), chat("taxes", 10, { archived: true }), chat("no-time", undefined)];
const entries = [entry("old-talk", "1970-01-01T00:00:00.020Z", { preview: "about the garden" }), entry("no-time", undefined)];

test("every chat and import, the newest first; one that says no time last, never the newest", () => {
assert.deepEqual(keys(historyRows({ chats, entries })), ["chat:trip", "imported:old-talk", "chat:taxes", "chat:no-time", "imported:no-time"]);
});

test("a chat's first record: a history card's time and newest words, a message's first line, none nothing", () => {
const card = { at: 7, body: { kind: "history", title: "t", summary: "the summary\nmore", messages: [{ role: "user", text: "first" }, { role: "assistant", text: "\n the last words\nand more" }] } };
assert.deepEqual(firstOf(card), { at: 7, line: "the last words" });
assert.deepEqual(firstOf({ at: 8, body: { kind: "history", summary: "only a summary", messages: [] } }), { at: 8, line: "only a summary" });
assert.deepEqual(firstOf({ at: 9, body: { text: "hello there\nsecond" } }), { at: 9, line: "hello there" });
assert.deepEqual(firstOf({ at: 10, body: { kind: "stop", turn: "x" } }), { at: 10, line: "" });
assert.equal(firstOf(undefined), null);
});

test("Archived lists the archived chats alone; Imported, the archive's conversations alone", () => {
assert.deepEqual(keys(historyRows({ chats, entries, filter: "archived" })), ["chat:taxes"]);
assert.deepEqual(keys(historyRows({ chats, entries, filter: "imported" })), ["imported:old-talk", "imported:no-time"]);
});

test("words: a title holding them all, an import's first words, or the search's finds, with its snippet", () => {
assert.deepEqual(keys(historyRows({ chats, entries, q: "TR" })), ["chat:trip"]);
assert.deepEqual(keys(historyRows({ chats, entries, q: "the garden" })), ["imported:old-talk"]);
assert.deepEqual(keys(historyRows({ chats, entries, q: "trip garden" })), []);
const found = historyRows({ chats, entries, q: "receipts", found: new Map([["taxes", "the receipts are in"]]) });
assert.deepEqual(found.map((r) => [r.key, r.snippet]), [["chat:taxes", "the receipts are in"]]);
const imported = historyRows({ chats, entries, q: "lunch", foundImported: new Map([["no-time", "lunch at noon"]]) });
assert.deepEqual(keys(imported), ["imported:no-time"]);
// a filter holds over what was found
assert.deepEqual(keys(historyRows({ chats, entries, q: "receipts", filter: "imported", found: new Map([["taxes", "x"]]) })), []);
});

test("the search's hint: eight words and 256 bytes at most, never a character cut", () => {
assert.equal(hintOf(" one two three "), "one two three");
assert.equal(hintOf("a b c d e f g h i j"), "a b c d e f g h");
const long = "é".repeat(200);
assert.ok(new TextEncoder().encode(hintOf(long)).length <= 256);
assert.equal(hintOf(long), "é".repeat(128));
});

test("a row's time: the time today, the day this year, with its year before it", () => {
const now = new Date(2026, 9, 31, 15, 0).getTime();
assert.equal(when(undefined, now), "");
assert.match(when(new Date(2026, 9, 31, 9, 5).getTime(), now), /9:05/);
assert.doesNotMatch(when(new Date(2026, 9, 12).getTime(), now), /2026/);
assert.match(when(new Date(2025, 9, 12).getTime(), now), /2025/);
});
Loading
Loading