This commit is contained in:
Dax Raad 2025-07-07 00:15:58 -04:00
parent 5fba41fe28
commit d02a6a8343
5 changed files with 177 additions and 152 deletions

View file

@ -129,9 +129,7 @@ export namespace Session {
id: Identifier.descending("session"), id: Identifier.descending("session"),
version: Installation.VERSION, version: Installation.VERSION,
parentID, parentID,
title: title: (parentID ? "Child session - " : "New Session - ") + new Date().toISOString(),
(parentID ? "Child session - " : "New Session - ") +
new Date().toISOString(),
time: { time: {
created: Date.now(), created: Date.now(),
updated: Date.now(), updated: Date.now(),
@ -220,9 +218,7 @@ export namespace Session {
} }
export async function getMessage(sessionID: string, messageID: string) { export async function getMessage(sessionID: string, messageID: string) {
return Storage.readJSON<MessageV2.Info>( return Storage.readJSON<MessageV2.Info>("session/message/" + sessionID + "/" + messageID)
"session/message/" + sessionID + "/" + messageID,
)
} }
export async function* list() { export async function* list() {
@ -274,10 +270,7 @@ export namespace Session {
} }
async function updateMessage(msg: MessageV2.Info) { async function updateMessage(msg: MessageV2.Info) {
await Storage.writeJSON( await Storage.writeJSON("session/message/" + msg.sessionID + "/" + msg.id, msg)
"session/message/" + msg.sessionID + "/" + msg.id,
msg,
)
Bus.publish(MessageV2.Event.Updated, { Bus.publish(MessageV2.Event.Updated, {
info: msg, info: msg,
}) })
@ -301,13 +294,8 @@ export namespace Session {
if (session.revert) { if (session.revert) {
const trimmed = [] const trimmed = []
for (const msg of msgs) { for (const msg of msgs) {
if ( if (msg.id > session.revert.messageID || (msg.id === session.revert.messageID && session.revert.part === 0)) {
msg.id > session.revert.messageID || await Storage.remove("session/message/" + input.sessionID + "/" + msg.id)
(msg.id === session.revert.messageID && session.revert.part === 0)
) {
await Storage.remove(
"session/message/" + input.sessionID + "/" + msg.id,
)
await Bus.publish(MessageV2.Event.Removed, { await Bus.publish(MessageV2.Event.Removed, {
sessionID: input.sessionID, sessionID: input.sessionID,
messageID: msg.id, messageID: msg.id,
@ -332,17 +320,10 @@ export namespace Session {
// auto summarize if too long // auto summarize if too long
if (previous) { if (previous) {
const tokens = const tokens =
previous.tokens.input + previous.tokens.input + previous.tokens.cache.read + previous.tokens.cache.write + previous.tokens.output
previous.tokens.cache.read +
previous.tokens.cache.write +
previous.tokens.output
if ( if (
model.info.limit.context && model.info.limit.context &&
tokens > tokens > Math.max((model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9, 0)
Math.max(
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9,
0,
)
) { ) {
await summarize({ await summarize({
sessionID: input.sessionID, sessionID: input.sessionID,
@ -353,9 +334,7 @@ export namespace Session {
} }
} }
const lastSummary = msgs.findLast( const lastSummary = msgs.findLast((msg) => msg.role === "assistant" && msg.summary === true)
(msg) => msg.role === "assistant" && msg.summary === true,
)
if (lastSummary) msgs = msgs.filter((msg) => msg.id >= lastSummary.id) if (lastSummary) msgs = msgs.filter((msg) => msg.id >= lastSummary.id)
const app = App.info() const app = App.info()
@ -384,12 +363,7 @@ export namespace Session {
return [ return [
{ {
type: "text", type: "text",
text: [ text: ["Called the Read tool on " + url.pathname, "<results>", text, "</results>"].join("\n"),
"Called the Read tool on " + url.pathname,
"<results>",
text,
"</results>",
].join("\n"),
}, },
] ]
} }
@ -401,9 +375,7 @@ export namespace Session {
}, },
{ {
type: "file", type: "file",
url: url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64url"),
`data:${part.mime};base64,` +
Buffer.from(await file.bytes()).toString("base64url"),
mime: part.mime, mime: part.mime,
filename: part.filename!, filename: part.filename!,
}, },
@ -500,8 +472,7 @@ export namespace Session {
messageID: next.id, messageID: next.id,
metadata: async (val) => { metadata: async (val) => {
const match = next.parts.find( const match = next.parts.find(
(p): p is MessageV2.ToolPart => (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === opts.toolCallId,
p.type === "tool" && p.id === opts.toolCallId,
) )
if (match && match.state.status === "running") { if (match && match.state.status === "running") {
match.state.title = val.title match.state.title = val.title
@ -567,11 +538,7 @@ export namespace Session {
async transformParams(args) { async transformParams(args) {
if (args.type === "stream") { if (args.type === "stream") {
// @ts-expect-error // @ts-expect-error
args.params.prompt = ProviderTransform.message( args.params.prompt = ProviderTransform.message(args.params.prompt, input.providerID, input.modelID)
args.params.prompt,
input.providerID,
input.modelID,
)
} }
return args.params return args.params
}, },
@ -607,10 +574,7 @@ export namespace Session {
break break
case "tool-call": { case "tool-call": {
const match = next.parts.find( const match = next.parts.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId)
(p): p is MessageV2.ToolPart =>
p.type === "tool" && p.id === value.toolCallId,
)
if (match) { if (match) {
match.state = { match.state = {
status: "running", status: "running",
@ -628,10 +592,7 @@ export namespace Session {
break break
} }
case "tool-result": { case "tool-result": {
const match = next.parts.find( const match = next.parts.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId)
(p): p is MessageV2.ToolPart =>
p.type === "tool" && p.id === value.toolCallId,
)
if (match && match.state.status === "running") { if (match && match.state.status === "running") {
match.state = { match.state = {
status: "completed", status: "completed",
@ -654,10 +615,7 @@ export namespace Session {
} }
case "tool-error": { case "tool-error": {
const match = next.parts.find( const match = next.parts.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId)
(p): p is MessageV2.ToolPart =>
p.type === "tool" && p.id === value.toolCallId,
)
if (match && match.state.status === "running") { if (match && match.state.status === "running") {
match.state = { match.state = {
status: "error", status: "error",
@ -696,16 +654,10 @@ export namespace Session {
).toObject() ).toObject()
break break
case e instanceof Error: case e instanceof Error:
next.error = new NamedError.Unknown( next.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
{ message: e.toString() },
{ cause: e },
).toObject()
break break
default: default:
next.error = new NamedError.Unknown( next.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
{ message: JSON.stringify(e) },
{ cause: e },
)
} }
Bus.publish(Event.Error, { Bus.publish(Event.Error, {
error: next.error, error: next.error,
@ -719,11 +671,7 @@ export namespace Session {
break break
case "finish-step": case "finish-step":
const usage = getUsage( const usage = getUsage(model.info, value.usage, value.providerMetadata)
model.info,
value.usage,
value.providerMetadata,
)
next.cost += usage.cost next.cost += usage.cost
next.tokens = usage.tokens next.tokens = usage.tokens
break break
@ -733,10 +681,10 @@ export namespace Session {
type: "text", type: "text",
text: "", text: "",
} }
next.parts.push(text)
break break
case "text": case "text":
if (text.text === "") next.parts.push(text)
text.text += value.text text.text += value.text
break break
@ -763,11 +711,7 @@ export namespace Session {
return next return next
} }
export async function revert(_input: { export async function revert(_input: { sessionID: string; messageID: string; part: number }) {
sessionID: string
messageID: string
part: number
}) {
// TODO // TODO
/* /*
const message = await getMessage(input.sessionID, input.messageID) const message = await getMessage(input.sessionID, input.messageID)
@ -804,23 +748,16 @@ export namespace Session {
const session = await get(sessionID) const session = await get(sessionID)
if (!session) return if (!session) return
if (!session.revert) return if (!session.revert) return
if (session.revert.snapshot) if (session.revert.snapshot) await Snapshot.restore(sessionID, session.revert.snapshot)
await Snapshot.restore(sessionID, session.revert.snapshot)
update(sessionID, (draft) => { update(sessionID, (draft) => {
draft.revert = undefined draft.revert = undefined
}) })
} }
export async function summarize(input: { export async function summarize(input: { sessionID: string; providerID: string; modelID: string }) {
sessionID: string
providerID: string
modelID: string
}) {
using abort = lock(input.sessionID) using abort = lock(input.sessionID)
const msgs = await messages(input.sessionID) const msgs = await messages(input.sessionID)
const lastSummary = msgs.findLast( const lastSummary = msgs.findLast((msg) => msg.role === "assistant" && msg.summary === true)?.id
(msg) => msg.role === "assistant" && msg.summary === true,
)?.id
const filtered = msgs.filter((msg) => !lastSummary || msg.id >= lastSummary) const filtered = msgs.filter((msg) => !lastSummary || msg.id >= lastSummary)
const model = await Provider.getModel(input.providerID, input.modelID) const model = await Provider.getModel(input.providerID, input.modelID)
const app = App.info() const app = App.info()
@ -930,11 +867,7 @@ export namespace Session {
} }
} }
function getUsage( function getUsage(model: ModelsDev.Model, usage: LanguageModelUsage, metadata?: ProviderMetadata) {
model: ModelsDev.Model,
usage: LanguageModelUsage,
metadata?: ProviderMetadata,
) {
const tokens = { const tokens = {
input: usage.inputTokens ?? 0, input: usage.inputTokens ?? 0,
output: usage.outputTokens ?? 0, output: usage.outputTokens ?? 0,
@ -951,16 +884,8 @@ export namespace Session {
cost: new Decimal(0) cost: new Decimal(0)
.add(new Decimal(tokens.input).mul(model.cost.input).div(1_000_000)) .add(new Decimal(tokens.input).mul(model.cost.input).div(1_000_000))
.add(new Decimal(tokens.output).mul(model.cost.output).div(1_000_000)) .add(new Decimal(tokens.output).mul(model.cost.output).div(1_000_000))
.add( .add(new Decimal(tokens.cache.read).mul(model.cost.cache_read ?? 0).div(1_000_000))
new Decimal(tokens.cache.read) .add(new Decimal(tokens.cache.write).mul(model.cost.cache_write ?? 0).div(1_000_000))
.mul(model.cost.cache_read ?? 0)
.div(1_000_000),
)
.add(
new Decimal(tokens.cache.write)
.mul(model.cost.cache_write ?? 0)
.div(1_000_000),
)
.toNumber(), .toNumber(),
tokens, tokens,
} }
@ -972,11 +897,7 @@ export namespace Session {
} }
} }
export async function initialize(input: { export async function initialize(input: { sessionID: string; modelID: string; providerID: string }) {
sessionID: string
modelID: string
providerID: string
}) {
const app = App.info() const app = App.info()
await Session.chat({ await Session.chat({
sessionID: input.sessionID, sessionID: input.sessionID,

View file

@ -324,6 +324,7 @@ export default function Share(props: {
} }
} }
} }
console.log(result.messages)
return result return result
}) })

View file

@ -7,6 +7,7 @@
&[data-flush="true"] { &[data-flush="true"] {
border: none; border: none;
background-color: transparent;
padding: 0 padding: 0
} }

View file

@ -122,15 +122,15 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: .375rem; gap: 0.375rem;
padding-bottom: 1rem; padding-bottom: 1rem;
[data-slot="provider"] { [data-slot="provider"] {
line-height: 18px; line-height: 18px;
font-size: .875rem; font-size: 0.875rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: -.5px; letter-spacing: -0.5px;
color: var(--sl-color-text-secondary) color: var(--sl-color-text-secondary);
} }
[data-slot="model"] { [data-slot="model"] {
@ -173,21 +173,20 @@
align-items: flex-start; align-items: flex-start;
gap: 0.375rem; gap: 0.375rem;
padding-bottom: 1rem; padding-bottom: 1rem;
} }
[data-component="tool-title"] { [data-component="tool-title"] {
line-height: 18px; line-height: 18px;
font-size: .875rem; font-size: 0.875rem;
color: var(--sl-color-text-secondary); color: var(--sl-color-text-secondary);
max-width: var(--md-tool-width); max-width: var(--md-tool-width);
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: .375rem; gap: 0.375rem;
[data-slot="name"] { [data-slot="name"] {
text-transform: uppercase; text-transform: uppercase;
letter-spacing: -.5px; letter-spacing: -0.5px;
} }
[data-slot="target"] { [data-slot="target"] {
@ -201,7 +200,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: .5rem; gap: 0.5rem;
} }
[data-component="todos"] { [data-component="todos"] {
@ -285,4 +284,38 @@
} }
} }
[data-component="terminal"] {
width: 100%;
max-width: var(--md-tool-width);
[data-slot="body"] {
display: flex;
flex-direction: column;
border: 1px solid var(--sl-color-divider);
border-radius: 0.25rem;
overflow: hidden;
}
[data-slot="header"] {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--sl-color-divider);
font-size: 0.75rem;
color: var(--sl-color-text-secondary);
}
[data-slot="content"] {
display: flex;
flex-direction: column;
&> :global(.astro-code) {
margin: 0;
border-radius: 0;
border: none;
}
[data-slot="error"] {
border-top: 1px solid var(--sl-color-divider);
}
}
}
} }

View file

@ -8,7 +8,7 @@ import { DateTime } from "luxon"
import CodeBlock from "../CodeBlock" import CodeBlock from "../CodeBlock"
import map from "lang-map" import map from "lang-map"
import type { Diagnostic } from "vscode-languageserver-types" import type { Diagnostic } from "vscode-languageserver-types"
import { BashTool, FallbackTool } from "./tool" import { FallbackTool } from "./tool"
import { ContentCode } from "./content-code" import { ContentCode } from "./content-code"
import { ContentDiff } from "./content-diff" import { ContentDiff } from "./content-diff"
@ -88,42 +88,89 @@ export function Part(props: PartProps) {
<div data-slot="model">{props.message.modelID}</div> <div data-slot="model">{props.message.modelID}</div>
</div> </div>
)} )}
{props.part.type === "tool" && props.part.state.status === "completed" && props.message.role === "assistant" && ( {props.part.type === "tool" &&
<div data-component="tool" data-tool={props.part.tool}> props.part.state.status === "completed" &&
<Switch> props.message.role === "assistant" && (
<Match when={props.part.tool === "grep"}> <div data-component="tool" data-tool={props.part.tool}>
<GrepTool message={props.message} id={props.part.id} tool={props.part.tool} state={props.part.state} /> <Switch>
</Match> <Match when={props.part.tool === "grep"}>
<Match when={props.part.tool === "glob"}> <GrepTool
<GlobTool message={props.message} id={props.part.id} tool={props.part.tool} state={props.part.state} /> message={props.message}
</Match> id={props.part.id}
<Match when={props.part.tool === "list"}> tool={props.part.tool}
<ListTool message={props.message} id={props.part.id} tool={props.part.tool} state={props.part.state} /> state={props.part.state}
</Match> />
<Match when={props.part.tool === "read"}> </Match>
<ReadTool message={props.message} id={props.part.id} tool={props.part.tool} state={props.part.state} /> <Match when={props.part.tool === "glob"}>
</Match> <GlobTool
<Match when={props.part.tool === "write"}> message={props.message}
<WriteTool message={props.message} id={props.part.id} tool={props.part.tool} state={props.part.state} /> id={props.part.id}
</Match> tool={props.part.tool}
<Match when={props.part.tool === "edit"}> state={props.part.state}
<EditTool message={props.message} id={props.part.id} tool={props.part.tool} state={props.part.state} /> />
</Match> </Match>
<Match when={props.part.tool === "bash"}> <Match when={props.part.tool === "list"}>
<BashTool id={props.part.id} tool={props.part.tool} state={props.part.state} /> <ListTool
</Match> message={props.message}
<Match when={props.part.tool === "todowrite"}> id={props.part.id}
<TodoWriteTool message={props.message} id={props.part.id} tool={props.part.tool} state={props.part.state} /> tool={props.part.tool}
</Match> state={props.part.state}
<Match when={props.part.tool === "webfetch"}> />
<WebFetchTool message={props.message} id={props.part.id} tool={props.part.tool} state={props.part.state} /> </Match>
</Match> <Match when={props.part.tool === "read"}>
<Match when={true}> <ReadTool
<FallbackTool id={props.part.id} tool={props.part.tool} state={props.part.state} /> message={props.message}
</Match> id={props.part.id}
</Switch> tool={props.part.tool}
</div> state={props.part.state}
)} />
</Match>
<Match when={props.part.tool === "write"}>
<WriteTool
message={props.message}
id={props.part.id}
tool={props.part.tool}
state={props.part.state}
/>
</Match>
<Match when={props.part.tool === "edit"}>
<EditTool
message={props.message}
id={props.part.id}
tool={props.part.tool}
state={props.part.state}
/>
</Match>
<Match when={props.part.tool === "bash"}>
<BashTool
id={props.part.id}
tool={props.part.tool}
state={props.part.state}
message={props.message}
/>
</Match>
<Match when={props.part.tool === "todowrite"}>
<TodoWriteTool
message={props.message}
id={props.part.id}
tool={props.part.tool}
state={props.part.state}
/>
</Match>
<Match when={props.part.tool === "webfetch"}>
<WebFetchTool
message={props.message}
id={props.part.id}
tool={props.part.tool}
state={props.part.state}
/>
</Match>
<Match when={true}>
<FallbackTool id={props.part.id} tool={props.part.tool} state={props.part.state} />
</Match>
</Switch>
</div>
)}
</div> </div>
</div> </div>
) )
@ -439,6 +486,28 @@ export function EditTool(props: ToolProps) {
) )
} }
export function BashTool(props: ToolProps) {
const command = () => props.state.metadata?.title
const result = () => props.state.metadata?.stdout
const error = () => props.state.metadata?.stderr
return (
<>
<div data-component="terminal" data-size="sm">
<div data-slot="body">
<div data-slot="header">
<span>{props.state.metadata.description}</span>
</div>
<div data-slot="content">
<ContentCode flush lang="bash" code={props.state.input.command} />
<ContentCode flush lang="console" code={result() || ""} />
</div>
</div>
</div>
</>
)
}
export function GlobTool(props: ToolProps) { export function GlobTool(props: ToolProps) {
const count = () => props.state.metadata?.count const count = () => props.state.metadata?.count
const pattern = () => props.state.input.pattern const pattern = () => props.state.input.pattern