Skip to content

Commit dd56900

Browse files
committed
wip2: logs as messages
1 parent bd3d15d commit dd56900

File tree

9 files changed

+287
-36
lines changed

9 files changed

+287
-36
lines changed

packages/blink/src/local/chat-manager.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isToolOrDynamicToolUIPart, type UIMessage } from "ai";
22
import type { Client } from "../agent/client";
3+
import type { Source } from "../react/use-logger";
34
import {
45
createDiskStore,
56
createDiskStoreWatcher,
@@ -14,6 +15,8 @@ import {
1415
type StoredMessage,
1516
} from "./types";
1617
import type { ID } from "../agent/types";
18+
import { stripVTControlCharacters } from "node:util";
19+
import { RWLock } from "./rw-lock";
1720

1821
export type ChatStatus = "idle" | "streaming" | "error";
1922

@@ -27,6 +30,7 @@ export interface ChatState {
2730
readonly streamingMessage?: StoredMessage;
2831
readonly loading: boolean;
2932
readonly queuedMessages: StoredMessage[];
33+
readonly queuedLogs: StoredMessage[];
3034
}
3135

3236
export interface ChatManagerOptions {
@@ -67,6 +71,7 @@ export class ChatManager {
6771
private streamingMessage: StoredMessage | undefined;
6872
private status: ChatStatus = "idle";
6973
private queue: StoredMessage[] = [];
74+
private logQueue: StoredMessage[] = [];
7075
private abortController: AbortController | undefined;
7176
private isProcessingQueue = false;
7277

@@ -87,7 +92,6 @@ export class ChatManager {
8792
this.serializeMessage = options.serializeMessage;
8893
this.filterMessages = options.filterMessages;
8994
this.onError = options.onError;
90-
9195
// Start disk watcher
9296
this.watcher = createDiskStoreWatcher<StoredChat>(options.chatsDirectory, {
9397
pollInterval: 1000,
@@ -187,6 +191,7 @@ export class ChatManager {
187191
streamingMessage: this.streamingMessage,
188192
loading: this.loading,
189193
queuedMessages: this.queue,
194+
queuedLogs: this.logQueue,
190195
};
191196
}
192197

@@ -288,6 +293,44 @@ export class ChatManager {
288293
}
289294
}
290295

296+
async queueLogMessage({
297+
message: logMessage,
298+
level,
299+
source,
300+
}: {
301+
message: string;
302+
level: "error" | "log";
303+
source: Source;
304+
}): Promise<void> {
305+
const formattedMessage = `(EDIT MODE NOTE) ${source === "agent" ? "The agent" : "The `blink dev` CLI"} printed the following ${level}:\n\`\`\`\n${stripVTControlCharacters(logMessage)}\n\`\`\`\n`;
306+
const message = {
307+
id: crypto.randomUUID(),
308+
created_at: new Date().toISOString(),
309+
role: "user",
310+
parts: [{ type: "text", text: formattedMessage }],
311+
metadata: {
312+
__blink_log: true,
313+
level,
314+
source,
315+
message: logMessage,
316+
},
317+
mode: "edit",
318+
} satisfies StoredMessage;
319+
this.logQueue.push(message);
320+
this.notifyListeners();
321+
}
322+
323+
private async flushLogQueue(
324+
locked: LockedStoreEntry<StoredChat>
325+
): Promise<void> {
326+
if (this.logQueue.length === 0) {
327+
return;
328+
}
329+
const messages = [...this.logQueue];
330+
this.logQueue = [];
331+
await this.upsertMessages(messages, locked);
332+
}
333+
291334
/**
292335
* Send a message to the agent
293336
*/
@@ -337,6 +380,7 @@ export class ChatManager {
337380
let locked: LockedStoreEntry<StoredChat> | undefined;
338381
try {
339382
locked = await this.chatStore.lock(this.chatId);
383+
await this.flushLogQueue(locked);
340384
let first = true;
341385
while (this.queue.length > 0 || first) {
342386
first = false;
@@ -484,6 +528,8 @@ export class ChatManager {
484528
this.status = "idle";
485529

486530
if (locked) {
531+
await this.flushLogQueue(locked);
532+
487533
this.chat.updated_at = new Date().toISOString();
488534
await locked.set(this.chat);
489535
await locked.release();
@@ -556,6 +602,7 @@ export class ChatManager {
556602
this.streamingMessage = undefined;
557603
this.status = "idle";
558604
this.queue = [];
605+
this.logQueue = [];
559606
}
560607

561608
private notifyListeners(): void {

packages/blink/src/local/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { UIMessage } from "ai";
22
import type { ID } from "../agent/types";
3+
import type { Source } from "../react/use-logger";
34

45
export interface StoredChat {
56
id: ID;
@@ -46,3 +47,24 @@ export function isStoredMessageMetadata(
4647
typeof metadata === "object" && metadata?.__blink_internal !== undefined
4748
);
4849
}
50+
51+
export function isLogMessage(
52+
message: StoredMessage
53+
): message is StoredMessage & {
54+
metadata: {
55+
__blink_log: true;
56+
level: "error" | "log";
57+
source: Source;
58+
message: string;
59+
};
60+
} {
61+
return (
62+
typeof message.metadata === "object" &&
63+
message.metadata !== null &&
64+
"__blink_log" in message.metadata &&
65+
message.metadata.__blink_log === true &&
66+
"level" in message.metadata &&
67+
"source" in message.metadata &&
68+
"message" in message.metadata
69+
);
70+
}

packages/blink/src/react/use-bundler.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, useMemo, useRef, useState } from "react";
22
import { resolveConfig, type BuildLog, type BuildResult } from "../build";
3+
import { Logger } from "./use-logger";
34

45
export type BundlerStatus = "building" | "success" | "error";
56

@@ -13,6 +14,7 @@ export interface BundlerContext {
1314

1415
export interface UseBundlerOptions {
1516
readonly directory: string;
17+
readonly logger: Logger;
1618
readonly onBuildStart?: () => void;
1719
readonly onBuildSuccess?: (
1820
result: BuildResult & { duration: number }
@@ -28,8 +30,15 @@ export interface UseBundlerOptions {
2830
*/
2931
export default function useBundler(options: UseBundlerOptions | string) {
3032
// Support both string (directory) and object (options) for backwards compatibility
31-
const opts = typeof options === "string" ? { directory: options } : options;
32-
const { directory, onBuildStart, onBuildSuccess, onBuildError } = opts;
33+
const opts =
34+
typeof options === "string"
35+
? {
36+
directory: options,
37+
logger: new Logger(async () => {}),
38+
}
39+
: options;
40+
const { directory, logger, onBuildStart, onBuildSuccess, onBuildError } =
41+
opts;
3342

3443
const config = useMemo(() => resolveConfig(directory), [directory]);
3544

@@ -80,7 +89,7 @@ export default function useBundler(options: UseBundlerOptions | string) {
8089
},
8190
})
8291
.catch((err) => {
83-
console.log("error", err);
92+
logger.error("system", "error", err);
8493
setStatus("error");
8594
setError(err);
8695
onBuildErrorRef.current?.(err);

packages/blink/src/react/use-chat.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface UseChatOptions {
3030
export interface UseChat extends ChatState {
3131
readonly sendMessage: (message: StoredMessage) => Promise<void>;
3232
readonly upsertMessage: (message: StoredMessage) => Promise<void>;
33+
readonly queueLogMessage: ChatManager["queueLogMessage"];
3334
readonly deleteMessage: (id: string) => Promise<void>;
3435
readonly stopStreaming: () => void;
3536
readonly resetChat: () => Promise<void>;
@@ -55,6 +56,7 @@ export default function useChat(options: UseChatOptions): UseChat {
5556
status: "idle",
5657
loading: true,
5758
queuedMessages: [],
59+
queuedLogs: [],
5860
});
5961

6062
// Create manager on mount or when chatId changes
@@ -108,6 +110,15 @@ export default function useChat(options: UseChatOptions): UseChat {
108110
}
109111
}, []);
110112

113+
const queueLogMessage = useCallback<ChatManager["queueLogMessage"]>(
114+
async (args) => {
115+
if (managerRef.current) {
116+
await managerRef.current.queueLogMessage(args);
117+
}
118+
},
119+
[]
120+
);
121+
111122
const stopStreaming = useCallback(() => {
112123
if (managerRef.current) {
113124
managerRef.current.stopStreaming();
@@ -142,6 +153,7 @@ export default function useChat(options: UseChatOptions): UseChat {
142153
...state,
143154
sendMessage,
144155
upsertMessage,
156+
queueLogMessage,
145157
stopStreaming,
146158
resetChat,
147159
clearQueue,

packages/blink/src/react/use-dev-mode.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { join } from "path";
77
import type { Client, CapabilitiesResponse } from "../agent/client";
88
import { getDevhookID, createDevhookID, hasDevhook } from "../cli/lib/devhook";
99
import { createLocalServer, type LocalServer } from "../local/server";
10-
import { isStoredMessageMetadata } from "../local/types";
10+
import { isLogMessage, isStoredMessageMetadata } from "../local/types";
1111
import type { BuildLog } from "../build";
1212
import type { ID, UIOptions, UIOptionsSchema } from "../agent/index.browser";
1313
import useOptions from "./use-options";
@@ -18,11 +18,13 @@ import useDevhook from "./use-devhook";
1818
import useDotenv from "./use-dotenv";
1919
import useEditAgent from "./use-edit-agent";
2020
import useAuth, { type UseAuth } from "./use-auth";
21+
import type { Logger } from "./use-logger";
2122

2223
export type DevMode = "run" | "edit";
2324

2425
export interface UseDevModeOptions {
2526
readonly directory: string;
27+
readonly logger: Logger;
2628
readonly onBuildStart?: () => void;
2729
readonly onBuildSuccess?: (result: { duration: number }) => void;
2830
readonly onBuildError?: (error: BuildLog) => void;
@@ -157,6 +159,7 @@ export default function useDevMode(options: UseDevModeOptions): UseDevMode {
157159
entry: entrypoint,
158160
} = useBundler({
159161
directory,
162+
logger: options.logger,
160163
onBuildStart: options.onBuildStart,
161164
onBuildSuccess: options.onBuildSuccess,
162165
onBuildError: options.onBuildError,
@@ -170,7 +173,7 @@ export default function useDevMode(options: UseDevModeOptions): UseDevMode {
170173
});
171174

172175
// Environment
173-
const dotenv = useDotenv(directory);
176+
const dotenv = useDotenv(directory, options.logger);
174177
const env = useMemo(() => {
175178
const blinkToken = auth.token;
176179
if (blinkToken) {
@@ -305,6 +308,9 @@ export default function useDevMode(options: UseDevModeOptions): UseDevMode {
305308
if (isStoredMessageMetadata(msg.metadata)) {
306309
return false;
307310
}
311+
if (isLogMessage(msg)) {
312+
return false;
313+
}
308314
// Filter out messages created in edit mode
309315
if (msg.mode === "edit") {
310316
return false;
@@ -372,6 +378,7 @@ export default function useDevMode(options: UseDevModeOptions): UseDevMode {
372378
const devhook = useDevhook({
373379
id: devhookID,
374380
directory,
381+
logger: options.logger,
375382
disabled: !capabilities?.request,
376383
onRequest: async (request) => {
377384
if (!agent) {
@@ -404,7 +411,11 @@ export default function useDevMode(options: UseDevModeOptions): UseDevMode {
404411

405412
return response;
406413
} catch (err) {
407-
console.error("Error sending request to user's agent:", err);
414+
options.logger.error(
415+
"system",
416+
"Error sending request to user's agent:",
417+
err
418+
);
408419
return new Response("Internal server error", { status: 500 });
409420
}
410421
},

packages/blink/src/react/use-devhook.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { lock, getLockInfo } from "../local/lockfile";
44
import { join } from "node:path";
55
import chalk from "chalk";
66
import { getDevhookID } from "../cli/lib/devhook";
7+
import type { Logger } from "./use-logger";
78

89
export interface UseDevhookOptions {
910
// ID can optionally be provided to identify the devhook.
@@ -12,6 +13,7 @@ export interface UseDevhookOptions {
1213
readonly disabled?: boolean;
1314
readonly onRequest: (request: Request) => Promise<Response>;
1415
readonly directory: string;
16+
readonly logger: Logger;
1517
}
1618

1719
export default function useDevhook(options: UseDevhookOptions) {
@@ -64,12 +66,14 @@ export default function useDevhook(options: UseDevhookOptions) {
6466
// Ignore errors reading lock info
6567
}
6668

67-
console.error(
69+
options.logger.error(
70+
"system",
6871
chalk.red(
6972
`\nError: Another ${chalk.bold("blink dev")} process is already running in this directory${pidMessage}.`
7073
)
7174
);
72-
console.error(
75+
options.logger.error(
76+
"system",
7377
chalk.red(`Please stop the other process and try again.\n`)
7478
);
7579
process.exit(1);
@@ -80,10 +84,12 @@ export default function useDevhook(options: UseDevhookOptions) {
8084
err && typeof err === "object" && "message" in err
8185
? String(err.message)
8286
: String(err);
83-
console.warn(
87+
options.logger.error(
88+
"system",
8489
chalk.yellow(`\nWarning: Failed to acquire devhook lock: ${message}`)
8590
);
86-
console.warn(
91+
options.logger.error(
92+
"system",
8793
chalk.yellow(
8894
`Continuing without lock. Multiple ${chalk.bold("blink dev")} processes may conflict with each other.\n`
8995
)
@@ -168,7 +174,11 @@ export default function useDevhook(options: UseDevhookOptions) {
168174
try {
169175
releaseLock();
170176
} catch (err) {
171-
console.warn("Failed to release devhook lock:", err);
177+
options.logger.error(
178+
"system",
179+
"Failed to release devhook lock:",
180+
err
181+
);
172182
}
173183
}
174184
};

packages/blink/src/react/use-dotenv.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { readFileSync, watch } from "fs";
33

44
import { useEffect, useState } from "react";
55
import { findNearestEntry } from "../build/util";
6+
import type { Logger } from "./use-logger";
67

78
export default function useDotenv(
89
directory: string,
10+
logger: Logger,
911
name: string = ".env.local"
1012
) {
1113
const [env, setEnv] = useState<Record<string, string>>({});
@@ -19,7 +21,7 @@ export default function useDotenv(
1921
const parsed = parse(contents);
2022
setEnv(parsed);
2123
} catch (error) {
22-
console.error(`Error reading ${name}:`, error);
24+
logger.error("system", `Error reading ${name}:`, error);
2325
setEnv({});
2426
}
2527
};

0 commit comments

Comments
 (0)