Skip to content

Commit 0df10f5

Browse files
immakermattyclaude
andcommitted
feat(lazurio): Buddy's T3 Code is Buddy's one conversation, framed by the bubble
Root decision 0210, plan DEV-6676 (row 25). With T3CODE_LAZURIO_BUDDY=1 the web client is Buddy's surface: a header with Mattyčus, the conversation's days in a pane where the window has room and a date line above each day's first message, and no thread list, new thread, fork, rollback or thread details card. `/` opens Buddy's one thread through the thread route, and a message sent during a turn steers it; queueing is the explicit alternative. In Buddy's bubble the surface speaks the frame's side of lazurio.buddy.* v1, written from the protocol's description: it says it is ready, keeps the last valid page context from its parent on the host's exact origin, and asks for its window's size from its header and on Escape, which asks for the panel unless a dialog, menu or list is open in the surface or something inside stopped the key (the editor marks every Escape as handled). A day in the pane turns off following the end first, as the timeline's minimap does, so the jump stays. In the bubble the surface wears the host page's appearance through T3's own appearance preference; where T3 cannot wear it (storage blocked, or a saved single-appearance theme), the document wears T3's standard palette in the host's appearance (request of 2026-10-25). The server holds the same for every client and agent. Decorators where it builds the orchestrator and the checkpoint store refuse forks, rollbacks, other threads and deleting Buddy's thread, and take no checkpoints in Buddy's project, which is the managed Folder's (T3CODE_LAZURIO_FOLDER=<Folder>/.agents/buddy) and else ~/Lazurio/.agents/buddy. A thread.create with Buddy's thread id passes only while Buddy's thread route makes the thread (MakingBuddysThread), so a client cannot spend the route's fixed command on a thread of its own. A startup phase before recovery turns off continuing threads after an update, the agent browser, and Codex and Claude, once, recorded in <state>/lazurio/buddy-surface.json so that later choices stay. `GET /api/lazurio/buddy/days` lists the conversation's days for the pane. The day lines and the day jump check Buddy mode once per timeline, for the timeline's own Environment, and the rows read it from the timeline's shared state, so no other Environment's timeline does that work or takes a jump. On v0.1.3 two web seams are rebuilt and one server refusal is added. - Upstream installs the chat route's shortcuts once at app start (pingdotgg#25366 removed ChatRouteGlobalShortcuts), so Buddy's condition joins their `when` in shortcuts/chatRouteCommands.ts: `isBuddySurfaceNow()` reads the primary server config from the app's atom registry when a key is pressed. Without it Shift+Mod+N on Buddy's surface would try a new thread that the server then refuses. - The timeline no longer takes onManualNavigation (pingdotgg#25463); the day jump is called below the timeline's own navigateTimeline and passes it, and the day line sits beside upstream's AudioPreviewUnmountContext (pingdotgg#21363). - Upstream's guest share of a thread (pingdotgg#24104) is refused in Buddy's T3 Code: thread.guest-share.open, .redeem and thread.guest-message.submit fail in the command guard, because Buddy's conversation belongs to a Personalspace, which is never shared (root decision 0091); closing a share stays possible. The root renders no GuestShareDialogHost on Buddy's surface. A test drives a guest share of Buddy's thread through the server's orchestrator. Settings and Usage are modals over the thread now (pingdotgg#24847); the prompt-link draft stays off Buddy's surface inside the rewritten ChatRouteLayout, and the first-run gate stays off on NEW's slimmer root. The guards are Effect.fn decorators (effectFnOpportunity), the two effects of BuddySurface.tsx carry justified react-effect-budget suppressions, and the file headers are cut to the four-line comment limit. Any other thread of Buddy's Environment, opened by a link or a typed address, leads to Buddy's thread as `/` does: the chat layout shows BuddySurfaceIndex, which asks the server for Buddy's thread and replaces the address, so that thread never renders under Buddy's presentation. A thread of another Environment stays T3's, and ChatView and the thread details ask about the thread's own Environment. A browser test opens such an address directly. On v0.1.4: the root keeps upstream's local-Environment switch beside the Buddy surface check (pingdotgg#26207); the timeline finds upstream's unmatched pending approvals (pingdotgg#26255) after the day lines and adds the day keys to upstream's list data next to its new footer. ThreadDetailsPanel no longer exports its props type (pingdotgg#26238), so Buddy's panel takes ComponentProps of it, and the effect suppressions use the renamed lint rule t3code/no-unreasoned-state-sync (pingdotgg#26161). The host appearance, which row 28's commit carried on v0.1.3, comes back to this row: T3 no longer re-applies its theme from useTheme's effect (pingdotgg#25007, pingdotgg#25461), so the stand-in follows every application of T3's saved theme through subscribeRenderedAppearance, and the theme tests apply T3's stored appearance as app start does. Upstream's linked Environments (pingdotgg#16655, pingdotgg#16684, pingdotgg#17129) let a peer read and send into this server's threads; a Personalspace is never shared (0091), so withoutLinkedEnvironments reads linkedEnvironmentsEnabled as false in Buddy's T3 Code whatever is stored, which turns off every upstream gate that reads it (peer-link OAuth, a linked client's /mcp, outbound links, link requests, remote tasks). Every settings write through it also stores the switch off, the startup's Buddy defaults included, so a value stored before Buddy is read as off and the next settings write through it stores it off (the settings file's own migration write at load keeps what is stored, which every read still masks); `t3 environment link`, which builds its own settings (cli/environment.ts), takes the same guard, so a command run without T3CODE_LAZURIO_BUDDY reads what Buddy's server stored. On v0.1.5 the imports in runtimeLayer.ts and ws.ts sit next to ScratchThreadPromotion (pingdotgg#26767). Upstream's pre-commit hook runs knip (pingdotgg#26167), so BUDDY_DAYS_ROUTE, withBuddyDefaults and buddySurfaceProtocol.ts's constants and helpers that only that file uses are module-private. On v0.1.6 a side chat (pingdotgg#26901) renders ChatView embedded, without the header, the branch restore host or the thread details; Buddy's hidden header and BuddyThreadDetailsPanel sit inside upstream's new embedded branches. Buddy's one conversation offers no side chat: sideChatsOffered is false on Buddy's surface, so neither the panel nor the slash menu offer one, and a typed /side or /btw gets "Buddy's conversation has no side chats." as the reason instead of reaching Buddy as text (the server would refuse its thread.fork anyway). The timeline's day lines sit before upstream's new set of matched approval requests (pingdotgg#27420). On v0.1.7: upstream now gives the CLI's settings layer the managed policy (`ManagedPolicy.layer`). The CLI keeps the Buddy guard around `ServerSettings.layer` and provides `ManagedPolicy.layer` beside the secret store and persistence. The startup test provides `ManagedPolicy.layerTest()` and `NodeCrypto.layer`, which Effect 4.1.2 no longer brings with `NodeServices.layer`. ServerSettingsService on v0.1.7 also carries the managed policy summary (managedPolicy), which clients read to lock their controls; withoutLinkedEnvironments passes it on, and buddySurfaceGuards.test.ts fails without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f4d1d4c commit 0df10f5

38 files changed

Lines changed: 2459 additions & 60 deletions

‎apps/server/src/cli/environment.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import * as ServerConfig from "../config.ts";
1616
import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
1717
import * as PeerLinks from "../peer/PeerLinks.ts";
1818
import * as ServerSettings from "../serverSettings.ts";
19+
import * as LazurioBuddyGuards from "../lazurio/buddySurfaceGuards.ts";
1920
import * as ManagedPolicy from "../policy/ManagedPolicy.ts";
2021
import * as SqlitePersistence from "../persistence/Sqlite.ts";
2122
import { authLocationFlags, type CliAuthLocationFlags, resolveCliAuthConfig } from "./config.ts";
@@ -44,7 +45,7 @@ const runWithPeerLinks = Effect.fn("environment.runWithPeerLinks")(function* <A,
4445
),
4546
Layer.provide(ServerEnvironment.layer.pipe(Layer.provide(ServerSecretStore.layer))),
4647
Layer.provide(
47-
ServerSettings.layer.pipe(
48+
LazurioBuddyGuards.withoutLinkedEnvironments(ServerSettings.layer).pipe(
4849
Layer.provide(
4950
Layer.mergeAll(
5051
ServerSecretStore.layer,

‎apps/server/src/lazurio/buddyPrompt.orchestrator.test.ts‎

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
CommandId,
1212
type ComposerContextRecord,
1313
MessageId,
14+
type ModelSelection,
1415
ProjectId,
1516
ThreadId,
1617
} from "@t3tools/contracts";
@@ -24,15 +25,17 @@ import { isTerminalRunStatus } from "../orchestration-v2/ThreadManagementService
2425
import { lazurioPageContextBlock } from "./buddyPrompt.ts";
2526
import {
2627
addProject,
28+
BUDDY,
2729
buddyModel,
2830
layerBuddyServer,
2931
receivedPrompts,
3032
} from "./buddySurface.testkit.ts";
33+
import { resolveBuddyThread } from "./buddySurfaceThread.ts";
3134

32-
const requestLog = NodePath.join(
33-
NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "buddy-prompt-orchestrator-")),
34-
"requests.jsonl",
35-
);
35+
const home = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "buddy-prompt-orchestrator-"));
36+
const buddyLayer = NodePath.join(home, "Lazurio", ".agents", "buddy");
37+
NodeFS.mkdirSync(buddyLayer, { recursive: true });
38+
const requestLog = NodePath.join(home, "requests.jsonl");
3639
const page = {
3740
v: 1,
3841
environment: { id: "vm-01.example", kind: "work", name: "Pracovní" },
@@ -48,7 +51,11 @@ const page = {
4851
const send = Effect.fn("test.buddyPrompt.send")(function* (
4952
threadId: ThreadId,
5053
ordinal: number,
51-
message: { readonly text: string; readonly records?: ReadonlyArray<ComposerContextRecord> },
54+
message: {
55+
readonly text: string;
56+
readonly records?: ReadonlyArray<ComposerContextRecord>;
57+
readonly modelSelection?: ModelSelection;
58+
},
5259
) {
5360
const orchestrator = yield* Orchestrator.OrchestratorV2;
5461
const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
@@ -75,7 +82,7 @@ const send = Effect.fn("test.buddyPrompt.send")(function* (
7582
? {}
7683
: { context: { version: 1 as const, records: [...message.records] } }),
7784
attachments: [],
78-
modelSelection: buddyModel,
85+
modelSelection: message.modelSelection ?? buddyModel,
7986
dispatchMode: { type: "start_immediately" },
8087
});
8188
yield* Effect.raceFirst(
@@ -87,25 +94,9 @@ const send = Effect.fn("test.buddyPrompt.send")(function* (
8794
describe("Buddy's T3 Code", () => {
8895
it.live("gives Hermes the person's words and the page as a Czech block, turn after turn", () =>
8996
Effect.gen(function* () {
90-
const orchestrator = yield* Orchestrator.OrchestratorV2;
91-
const projectId = ProjectId.make("project:buddy");
92-
// Buddy's project is the Buddy layer, a folder without Git.
93-
yield* addProject(projectId, NodePath.dirname(requestLog));
94-
const threadId = ThreadId.make("thread:buddy-prompt");
95-
yield* orchestrator.dispatch({
96-
type: "thread.create",
97-
createdBy: "user",
98-
creationSource: "web",
99-
commandId: CommandId.make("command:buddy-prompt:create"),
100-
threadId,
101-
projectId,
102-
title: "Buddy",
103-
modelSelection: buddyModel,
104-
runtimeMode: "full-access",
105-
interactionMode: "default",
106-
branch: null,
107-
worktreePath: null,
108-
});
97+
// Buddy's project is Buddy's layer, a folder without Git, and the thread is Buddy's own.
98+
yield* addProject(ProjectId.make("project:buddy"), buddyLayer);
99+
const { threadId } = yield* resolveBuddyThread();
109100
const record = {
110101
version: 1,
111102
kind: "lazurio-page",
@@ -128,9 +119,52 @@ describe("Buddy's T3 Code", () => {
128119
Effect.scoped,
129120
Effect.provideService(HostProcess.Environment, {
130121
...process.env,
122+
HOME: home,
131123
T3CODE_LAZURIO_BUDDY: "1",
132124
T3_ACP_REQUEST_LOG_PATH: requestLog,
133125
}),
134126
),
135127
);
136128
});
129+
130+
describe("Buddy's T3 Code and Hermes's settings", () => {
131+
it.live("sets Hermes no model, mode or other option a message's selection names", () => {
132+
const selectionHome = NodeFS.mkdtempSync(
133+
NodePath.join(NodeOS.tmpdir(), "buddy-selection-orchestrator-"),
134+
);
135+
const selectionLayer = NodePath.join(selectionHome, "Lazurio", ".agents", "buddy");
136+
NodeFS.mkdirSync(selectionLayer, { recursive: true });
137+
const selectionLog = NodePath.join(selectionHome, "requests.jsonl");
138+
return Effect.gen(function* () {
139+
yield* addProject(ProjectId.make("project:buddy"), selectionLayer);
140+
const { threadId } = yield* resolveBuddyThread();
141+
// As the web sends it: Hermes's current model and remembered options in place of "default".
142+
yield* send(threadId, 1, {
143+
text: "Ahoj.",
144+
modelSelection: {
145+
instanceId: BUDDY,
146+
model: "composer-2",
147+
options: [
148+
{ id: "mode", value: "code" },
149+
{ id: "fast", value: "fast" },
150+
],
151+
},
152+
});
153+
assert.deepStrictEqual(receivedPrompts(selectionLog), [["Ahoj."]]);
154+
const methods = NodeFS.readFileSync(selectionLog, "utf8")
155+
.split("\n")
156+
.flatMap((line) => (line.trim() === "" ? [] : [JSON.parse(line).method]));
157+
assert.notInclude(methods, "session/set_config_option");
158+
assert.notInclude(methods, "session/set_model");
159+
}).pipe(
160+
Effect.provide(layerBuddyServer),
161+
Effect.scoped,
162+
Effect.provideService(HostProcess.Environment, {
163+
...process.env,
164+
HOME: selectionHome,
165+
T3CODE_LAZURIO_BUDDY: "1",
166+
T3_ACP_REQUEST_LOG_PATH: selectionLog,
167+
}),
168+
);
169+
});
170+
});

‎apps/server/src/lazurio/buddySurface.testkit.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export const layerBuddyServer = Layer.mergeAll(
140140
Layer.provide(McpSessionRegistryTestkit.layer),
141141
Layer.provide(SqlitePersistence.layerMemory),
142142
Layer.provide(layerCheckpointStore),
143-
Layer.provide(layerServerConfig),
143+
Layer.provideMerge(layerServerConfig),
144144
Layer.provide(layerServerSettings),
145145
Layer.provide(layerProviderInstanceRegistry),
146146
Layer.provide(ResetCreditCoordinator.layer),

‎apps/server/src/lazurio/buddySurface.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import { ProviderInstanceId } from "@t3tools/contracts";
88
import * as HostProcess from "@t3tools/shared/HostProcess";
99
import * as Effect from "effect/Effect";
10+
import * as Path from "effect/Path";
11+
12+
import * as ServerConfig from "../config.ts";
1013

1114
const LAZURIO_BUDDY_ENV = "T3CODE_LAZURIO_BUDDY";
1215

@@ -25,3 +28,25 @@ const isBuddySurface = Effect.gen(function* () {
2528
/** Whether this instance is Buddy's: the instance `buddy` of a server that is Buddy's. */
2629
export const isBuddyInstance = (environment: NodeJS.ProcessEnv, instanceId: string): boolean =>
2730
isBuddySurfaceEnvironment(environment) && instanceId === BUDDY_PROVIDER_INSTANCE_ID;
31+
32+
/** Buddy's T3 Code in the config the server gives its clients (`server.getConfig`). */
33+
export const buddySurfaceClientConfig = Effect.map(isBuddySurface, (buddy) =>
34+
buddy ? { lazurioBuddy: true as const } : {},
35+
);
36+
37+
/**
38+
* Buddy's project folder, Buddy's layer of the Lazurio Folder. Machines declare it as the managed
39+
* Folder of Buddy's T3 Code (`T3CODE_LAZURIO_FOLDER`), whose startup adopts or creates its project
40+
* (managedFolderStartup.ts); without that, `~/Lazurio/.agents/buddy` of the person the server runs
41+
* as. Null when neither is known.
42+
*/
43+
export const buddyProjectFolder = Effect.gen(function* () {
44+
const { lazurioFolder } = yield* ServerConfig.ServerConfig;
45+
const path = yield* Path.Path;
46+
// Upstream's tests mock the config, and a mock answers a field it lacks with a function.
47+
if (typeof lazurioFolder === "string") return path.resolve(lazurioFolder);
48+
const home = (yield* HostProcess.Environment).HOME;
49+
return home === undefined || home === ""
50+
? null
51+
: path.resolve(home, "Lazurio", ".agents", "buddy");
52+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { assert, describe, it } from "@effect/vitest";
2+
import { MessageId } from "@t3tools/contracts";
3+
import * as DateTime from "effect/DateTime";
4+
5+
import { buddyDaysOf, timeZoneOf } from "./buddySurfaceDays.ts";
6+
7+
const message = (id: string, iso: string, text: string) => ({
8+
id: MessageId.make(id),
9+
text,
10+
createdAt: DateTime.makeUnsafe(iso),
11+
});
12+
13+
describe("the days of Buddy's conversation", () => {
14+
it("are the person's local days, newest first, each with its first message", () => {
15+
const messages = [
16+
message("m1", "2026-10-21T08:00:00.000Z", "Dobré ráno.\nCo dnes?"),
17+
// 00:30 on 22 October in Prague, still 21 October in UTC.
18+
message(
19+
"m2",
20+
"2026-10-21T22:30:00.000Z",
21+
"Ještě něco [Kontext stránky](t3-context://v1/lazurio-page/p-1) prosím",
22+
),
23+
message("m3", "2026-10-22T09:00:00.000Z", "Druhá zpráva dne."),
24+
];
25+
assert.deepStrictEqual(buddyDaysOf(messages, "Europe/Prague"), [
26+
{ day: "2026-10-22", firstMessageId: "m2", preview: "Ještě něco prosím", messages: 2 },
27+
{ day: "2026-10-21", firstMessageId: "m1", preview: "Dobré ráno.", messages: 1 },
28+
]);
29+
assert.deepStrictEqual(
30+
buddyDaysOf(messages, "UTC").map((day) => [day.day, day.messages]),
31+
[
32+
["2026-10-22", 1],
33+
["2026-10-21", 2],
34+
],
35+
);
36+
});
37+
38+
it("caps a long first line and keeps an empty message's day", () => {
39+
const [day] = buddyDaysOf(
40+
[message("m1", "2026-10-22T09:00:00.000Z", `\n\n${"a".repeat(200)}`)],
41+
"UTC",
42+
);
43+
assert.strictEqual(day?.preview.length, 120);
44+
assert.isTrue(day?.preview.endsWith("…"));
45+
assert.strictEqual(
46+
buddyDaysOf([message("m2", "2026-10-22T09:00:00.000Z", "")], "UTC")[0]?.preview,
47+
"",
48+
);
49+
});
50+
51+
it("counts a zone the runtime does not know as UTC", () => {
52+
assert.strictEqual(timeZoneOf("Europe/Prague"), "Europe/Prague");
53+
assert.strictEqual(timeZoneOf("Mars/Olympus"), "UTC");
54+
assert.strictEqual(timeZoneOf(null), "UTC");
55+
});
56+
});
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* Lazurio overlay (root decision 0210, plan DEV-6676): `GET /api/lazurio/buddy/days?tz=<zone>`
3+
* lists the local days on which a message reached Buddy, newest first, each with its first message
4+
* and first line, for the surface's day pane. 404 outside Buddy's T3 Code; no days before Buddy's
5+
* thread exists.
6+
*/
7+
import {
8+
AuthOrchestrationReadScope,
9+
type MessageId,
10+
type OrchestrationV2ConversationMessage,
11+
} from "@t3tools/contracts";
12+
import { collectComposerContextReferences } from "@t3tools/shared/composerContextReferences";
13+
import * as HostProcess from "@t3tools/shared/HostProcess";
14+
import * as DateTime from "effect/DateTime";
15+
import * as Effect from "effect/Effect";
16+
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
17+
18+
import { OrchestratorV2 } from "../orchestration-v2/Orchestrator.ts";
19+
import { isBuddySurfaceEnvironment } from "./buddySurface.ts";
20+
import { BUDDY_THREAD_ID } from "./buddySurfaceThread.ts";
21+
import { authenticateOverlayRoute } from "./routeAuth.ts";
22+
23+
const BUDDY_DAYS_ROUTE = "/api/lazurio/buddy/days";
24+
25+
const PREVIEW_MAX = 120;
26+
27+
export interface BuddyDay {
28+
/** The local date, `YYYY-MM-DD`. */
29+
readonly day: string;
30+
readonly firstMessageId: MessageId;
31+
readonly preview: string;
32+
readonly messages: number;
33+
}
34+
35+
/** The zone a request names, if the runtime knows it; UTC otherwise. */
36+
export function timeZoneOf(value: string | null | undefined): string {
37+
if (value === null || value === undefined || value === "") return "UTC";
38+
try {
39+
return new Intl.DateTimeFormat("en-CA", { timeZone: value }).resolvedOptions().timeZone;
40+
} catch {
41+
return "UTC";
42+
}
43+
}
44+
45+
/** A message's first line, its context links taken out, as the pane shows it. */
46+
function previewOf(text: string): string {
47+
let plain = text;
48+
for (const occurrence of collectComposerContextReferences(text).toReversed())
49+
plain = plain.slice(0, occurrence.start) + plain.slice(occurrence.end);
50+
const line = plain.split("\n").find((candidate) => candidate.trim() !== "") ?? "";
51+
const trimmed = line.trim().replace(/\s+/g, " ");
52+
return trimmed.length > PREVIEW_MAX ? `${trimmed.slice(0, PREVIEW_MAX - 1)}…` : trimmed;
53+
}
54+
55+
/** The person's messages grouped by local day in `timeZone`, newest day first. */
56+
export function buddyDaysOf(
57+
messages: ReadonlyArray<Pick<OrchestrationV2ConversationMessage, "id" | "text" | "createdAt">>,
58+
timeZone: string,
59+
): Array<BuddyDay> {
60+
const format = new Intl.DateTimeFormat("en-CA", {
61+
timeZone,
62+
year: "numeric",
63+
month: "2-digit",
64+
day: "2-digit",
65+
});
66+
const days = new Map<
67+
string,
68+
{ first: (typeof messages)[number]; firstAt: number; count: number }
69+
>();
70+
for (const message of messages) {
71+
const at = DateTime.toEpochMillis(message.createdAt);
72+
const day = format.format(at);
73+
const current = days.get(day);
74+
if (current === undefined) days.set(day, { first: message, firstAt: at, count: 1 });
75+
else {
76+
current.count += 1;
77+
if (at < current.firstAt) {
78+
current.first = message;
79+
current.firstAt = at;
80+
}
81+
}
82+
}
83+
return [...days.entries()]
84+
.toSorted(([left], [right]) => right.localeCompare(left))
85+
.map(([day, { first, count }]) => ({
86+
day,
87+
firstMessageId: first.id,
88+
preview: previewOf(first.text),
89+
messages: count,
90+
}));
91+
}
92+
93+
export const layer = HttpRouter.use((router) =>
94+
Effect.gen(function* () {
95+
// A handler sees only the request's context, so it takes the orchestrator from the layer's.
96+
const orchestrator = yield* OrchestratorV2;
97+
yield* router.add(
98+
"GET",
99+
BUDDY_DAYS_ROUTE,
100+
Effect.gen(function* () {
101+
yield* authenticateOverlayRoute(AuthOrchestrationReadScope);
102+
if (!isBuddySurfaceEnvironment(yield* HostProcess.Environment))
103+
return HttpServerResponse.jsonUnsafe({ error: "not-buddy" }, { status: 404 });
104+
const request = yield* HttpServerRequest.HttpServerRequest;
105+
const timeZone = timeZoneOf(new URL(request.url, "http://local").searchParams.get("tz"));
106+
const shell = yield* orchestrator.getThreadShell(BUDDY_THREAD_ID).pipe(Effect.orDie);
107+
if (shell === null) return HttpServerResponse.jsonUnsafe({ days: [] });
108+
const records = yield* orchestrator
109+
.getThreadRecords(BUDDY_THREAD_ID, ["messages"], { messageRoles: ["user"] })
110+
.pipe(Effect.orDie);
111+
return HttpServerResponse.jsonUnsafe({ days: buddyDaysOf(records.messages, timeZone) });
112+
}),
113+
);
114+
}),
115+
);

0 commit comments

Comments
 (0)