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

View file

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

View file

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

View file

@ -122,15 +122,15 @@
display: flex;
flex-direction: column;
align-items: flex-start;
gap: .375rem;
gap: 0.375rem;
padding-bottom: 1rem;
[data-slot="provider"] {
line-height: 18px;
font-size: .875rem;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: -.5px;
color: var(--sl-color-text-secondary)
letter-spacing: -0.5px;
color: var(--sl-color-text-secondary);
}
[data-slot="model"] {
@ -173,21 +173,20 @@
align-items: flex-start;
gap: 0.375rem;
padding-bottom: 1rem;
}
[data-component="tool-title"] {
line-height: 18px;
font-size: .875rem;
font-size: 0.875rem;
color: var(--sl-color-text-secondary);
max-width: var(--md-tool-width);
display: flex;
align-items: flex-start;
gap: .375rem;
gap: 0.375rem;
[data-slot="name"] {
text-transform: uppercase;
letter-spacing: -.5px;
letter-spacing: -0.5px;
}
[data-slot="target"] {
@ -201,7 +200,7 @@
display: flex;
flex-direction: column;
align-items: flex-start;
gap: .5rem;
gap: 0.5rem;
}
[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 map from "lang-map"
import type { Diagnostic } from "vscode-languageserver-types"
import { BashTool, FallbackTool } from "./tool"
import { FallbackTool } from "./tool"
import { ContentCode } from "./content-code"
import { ContentDiff } from "./content-diff"
@ -88,42 +88,89 @@ export function Part(props: PartProps) {
<div data-slot="model">{props.message.modelID}</div>
</div>
)}
{props.part.type === "tool" && props.part.state.status === "completed" && props.message.role === "assistant" && (
<div data-component="tool" data-tool={props.part.tool}>
<Switch>
<Match when={props.part.tool === "grep"}>
<GrepTool message={props.message} id={props.part.id} tool={props.part.tool} state={props.part.state} />
</Match>
<Match when={props.part.tool === "glob"}>
<GlobTool message={props.message} id={props.part.id} tool={props.part.tool} state={props.part.state} />
</Match>
<Match when={props.part.tool === "list"}>
<ListTool message={props.message} id={props.part.id} tool={props.part.tool} state={props.part.state} />
</Match>
<Match when={props.part.tool === "read"}>
<ReadTool message={props.message} id={props.part.id} tool={props.part.tool} 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} />
</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>
)}
{props.part.type === "tool" &&
props.part.state.status === "completed" &&
props.message.role === "assistant" && (
<div data-component="tool" data-tool={props.part.tool}>
<Switch>
<Match when={props.part.tool === "grep"}>
<GrepTool
message={props.message}
id={props.part.id}
tool={props.part.tool}
state={props.part.state}
/>
</Match>
<Match when={props.part.tool === "glob"}>
<GlobTool
message={props.message}
id={props.part.id}
tool={props.part.tool}
state={props.part.state}
/>
</Match>
<Match when={props.part.tool === "list"}>
<ListTool
message={props.message}
id={props.part.id}
tool={props.part.tool}
state={props.part.state}
/>
</Match>
<Match when={props.part.tool === "read"}>
<ReadTool
message={props.message}
id={props.part.id}
tool={props.part.tool}
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>
)
@ -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) {
const count = () => props.state.metadata?.count
const pattern = () => props.state.input.pattern