Skip to content

Commit 3f40823

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

File tree

4 files changed

+193
-12
lines changed

4 files changed

+193
-12
lines changed

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface ChatState {
2727
readonly streamingMessage?: StoredMessage;
2828
readonly loading: boolean;
2929
readonly queuedMessages: StoredMessage[];
30+
readonly queuedLogs: StoredMessage[];
3031
}
3132

3233
export interface ChatManagerOptions {
@@ -67,6 +68,7 @@ export class ChatManager {
6768
private streamingMessage: StoredMessage | undefined;
6869
private status: ChatStatus = "idle";
6970
private queue: StoredMessage[] = [];
71+
private logQueue: StoredMessage[] = [];
7072
private abortController: AbortController | undefined;
7173
private isProcessingQueue = false;
7274

@@ -187,6 +189,7 @@ export class ChatManager {
187189
streamingMessage: this.streamingMessage,
188190
loading: this.loading,
189191
queuedMessages: this.queue,
192+
queuedLogs: this.logQueue,
190193
};
191194
}
192195

@@ -288,6 +291,34 @@ export class ChatManager {
288291
}
289292
}
290293

294+
/**
295+
* Queue a log message to be inserted after streaming completes,
296+
* or insert immediately if not streaming.
297+
*/
298+
async queueLogMessage(args: {
299+
message: string;
300+
level: "error" | "log";
301+
source: string;
302+
}): Promise<void> {
303+
const message = {
304+
id: crypto.randomUUID(),
305+
created_at: new Date().toISOString(),
306+
role: "user",
307+
parts: [{ type: "text", text: args.message }],
308+
metadata: {
309+
__blink_log: true,
310+
level: args.level,
311+
source: args.source,
312+
},
313+
mode: "run",
314+
} satisfies StoredMessage;
315+
if (this.isProcessingQueue) {
316+
this.logQueue.push(message);
317+
} else {
318+
await this.upsertMessages([message]);
319+
}
320+
}
321+
291322
/**
292323
* Send a message to the agent
293324
*/
@@ -484,6 +515,13 @@ export class ChatManager {
484515
this.status = "idle";
485516

486517
if (locked) {
518+
// Flush log queue before releasing lock
519+
if (this.logQueue.length > 0) {
520+
const logs = [...this.logQueue];
521+
this.logQueue = [];
522+
await this.upsertMessages(logs, locked);
523+
}
524+
487525
this.chat.updated_at = new Date().toISOString();
488526
await locked.set(this.chat);
489527
await locked.release();

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,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { createContext, useContext } from "react";
2+
3+
// This lets us serialize log writes without using async log and error methods.
4+
let logQueue: Promise<void> = Promise.resolve();
5+
6+
export class Logger {
7+
constructor(
8+
private printLog: (
9+
level: "error" | "log",
10+
source: string,
11+
...message: unknown[]
12+
) => Promise<void>
13+
) {}
14+
15+
error(source: string, ...message: [unknown, ...unknown[]]): void {
16+
logQueue = logQueue.then(() =>
17+
this.printLog("error", source, ...message).catch((err) => {
18+
console.error("Error printing log:", err);
19+
})
20+
);
21+
}
22+
23+
log(source: string, ...message: [unknown, ...unknown[]]): void {
24+
logQueue = logQueue.then(() =>
25+
this.printLog("log", source, ...message).catch((err) => {
26+
console.error("Error printing log:", err);
27+
})
28+
);
29+
}
30+
31+
flush(): Promise<void> {
32+
return logQueue;
33+
}
34+
}
35+
36+
export const LoggerContext = createContext<Logger | undefined>(undefined);
37+
38+
export const useLogger = (): Logger => {
39+
const logger = useContext(LoggerContext);
40+
if (!logger) {
41+
throw new Error("useLogger must be used within a LoggerProvider");
42+
}
43+
return logger;
44+
};

packages/blink/src/tui/dev.tsx

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,48 @@ import { render } from "ink";
2424
import type { StoredMessage } from "../local/types";
2525
import type { ID } from "../agent/types";
2626
import { checkAndMarkFirstRun } from "../cli/lib/first-run";
27+
import type { UseChat } from "../react/use-chat";
28+
import util from "node:util";
29+
import { useLogger } from "../react/use-logger";
30+
import { Logger, LoggerContext } from "../react/use-logger";
2731

2832
const colors = {
2933
run: "#1f86ed",
3034
edit: "#e8900e",
3135
} as const;
3236

3337
export async function startDev({ directory }: { directory: string }) {
34-
const instance = render(
35-
<KeypressProvider>
36-
<Root directory={directory} />
37-
</KeypressProvider>,
38-
{
39-
exitOnCtrlC: false,
40-
}
41-
);
38+
const instance = render(<Root directory={directory} />, {
39+
exitOnCtrlC: false,
40+
});
4241
await instance.waitUntilExit();
4342
}
4443

4544
const Root = ({ directory }: { directory: string }) => {
45+
const [logger, setLogger] = useState<Logger>(
46+
new Logger(async (level, ...message) => {
47+
console[level](...message);
48+
})
49+
);
50+
return (
51+
<KeypressProvider>
52+
<LoggerContext.Provider value={logger}>
53+
<App directory={directory} setLogger={setLogger} />
54+
</LoggerContext.Provider>
55+
</KeypressProvider>
56+
);
57+
};
58+
59+
const App = ({
60+
directory,
61+
setLogger,
62+
}: {
63+
directory: string;
64+
setLogger: (logger: Logger) => void;
65+
}) => {
4666
const size = useTerminalSize();
4767
const [isFirstRun] = useState(() => checkAndMarkFirstRun(directory));
68+
const logger = useLogger();
4869

4970
// Use the shared dev mode hook
5071
const dev = useDevMode({
@@ -73,10 +94,11 @@ const Root = ({ directory }: { directory: string }) => {
7394
console.log(chalk.gray(`⚙ Send webhooks from anywhere: ${url}`));
7495
},
7596
onAgentLog: (log) => {
76-
const logColor = log.level === "error" ? "red" : "white";
77-
const logPrefix =
78-
log.level === "error" ? "⚙ [Agent Error]" : "⚙ [Agent Log]";
79-
console.log(`${chalk[logColor](logPrefix)} ${chalk.gray(log.message)}`);
97+
if (log.level === "error") {
98+
logger.error("agent", log.message);
99+
} else {
100+
logger.log("agent", log.message);
101+
}
80102
},
81103
onDevhookRequest: (request) => {
82104
console.log(
@@ -102,6 +124,17 @@ const Root = ({ directory }: { directory: string }) => {
102124
}
103125
},
104126
});
127+
useEffect(() => {
128+
setLogger(
129+
new Logger((level, source, ...message) => {
130+
return dev.chat.queueLogMessage({
131+
message: util.format(...message),
132+
level,
133+
source,
134+
});
135+
})
136+
);
137+
}, [dev.chat.queueLogMessage, setLogger]);
105138

106139
const { exit } = useApp();
107140
const [exitArmed, setExitArmed] = useState(false);
@@ -324,6 +357,20 @@ const Root = ({ directory }: { directory: string }) => {
324357
maxWidth={size.columns - 2}
325358
/>
326359
) : null}
360+
{dev.chat.queuedLogs.map((log) => (
361+
<Message
362+
key={log.id}
363+
message={log}
364+
nextMessage={undefined}
365+
previousMessage={
366+
dev.chat.streamingMessage ||
367+
(dev.chat.messages.length > 0
368+
? dev.chat.messages.at(dev.chat.messages.length - 1)
369+
: undefined)
370+
}
371+
maxWidth={size.columns - 2}
372+
/>
373+
))}
327374
{dev.showWaitingPlaceholder ? (
328375
<AssistantWaitingPlaceholder maxWidth={size.columns - 2} />
329376
) : null}
@@ -632,12 +679,52 @@ const MessageComponent = ({
632679
maxWidth?: number;
633680
streaming?: boolean;
634681
}) => {
682+
// Check if this is a log message
683+
const isLogMessage =
684+
message.metadata &&
685+
typeof message.metadata === "object" &&
686+
"__blink_log" in message.metadata &&
687+
message.metadata.__blink_log === true;
688+
635689
let prefix: React.ReactNode;
636690
let contentColor: string;
637691
// Only add margin if there is a previous message.
638692
// Otherwise, we end up with two blank lines under the banner.
639693
let marginTop: number = previousMessage ? 1 : 0;
640694

695+
if (isLogMessage) {
696+
// Format log messages with special styling
697+
const logLevel = (message.metadata as any).level;
698+
const logSource = (message.metadata as any).source;
699+
const isError = logLevel === "error";
700+
701+
prefix = null;
702+
contentColor = isError ? "red" : "gray";
703+
704+
// Render log messages with a different structure
705+
const content: React.ReactNode = (
706+
<Box gap={1} flexDirection="column" width={maxWidth}>
707+
{message.parts
708+
.map((part, index) => {
709+
if (part.type === "text") {
710+
return <Text color="gray">{part.text}</Text>;
711+
}
712+
return null;
713+
})
714+
.filter(Boolean)}
715+
</Box>
716+
);
717+
718+
return (
719+
<Box flexDirection="row" gap={1}>
720+
<Text color={isError ? "red" : undefined} bold>
721+
[{logSource}]
722+
</Text>
723+
{content}
724+
</Box>
725+
);
726+
}
727+
641728
switch (message.role) {
642729
case "system":
643730
prefix = <Text>t </Text>;

0 commit comments

Comments
 (0)