+
@@ -243,7 +244,32 @@ export default function SessionTimeline(props: { session: string; class?: string
const size = createElementSize(root)
const scroll = createScrollPosition(scrollElement)
- onMount(() => sync.session.sync(props.session))
+ const valid = (part: Part) => {
+ if (!part) return false
+ switch (part.type) {
+ case "step-start":
+ case "step-finish":
+ case "file":
+ case "patch":
+ return false
+ case "text":
+ return !part.synthetic
+ case "reasoning":
+ return part.text.trim()
+ case "tool":
+ switch (part.tool) {
+ case "todoread":
+ case "todowrite":
+ case "list":
+ case "grep":
+ return false
+ }
+ return true
+ default:
+ return true
+ }
+ }
+
const session = createMemo(() => sync.session.get(props.session))
const messages = createMemo(() => sync.data.message[props.session] ?? [])
const working = createMemo(() => {
@@ -253,6 +279,45 @@ export default function SessionTimeline(props: { session: string; class?: string
return !last.time.completed
})
+ const cost = createMemo(() => {
+ const total = pipe(
+ messages(),
+ sumBy((x) => (x.role === "assistant" ? x.cost : 0)),
+ )
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ }).format(total)
+ })
+
+ const last = createMemo(() => {
+ return messages().findLast((x) => x.role === "assistant") as AssistantMessage
+ })
+
+ const model = createMemo(() => {
+ if (!last()) return
+ const model = sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID]
+ return model
+ })
+
+ const tokens = createMemo(() => {
+ if (!last()) return
+ const tokens = last().tokens
+ const total = tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
+ return new Intl.NumberFormat("en-US", {
+ notation: "compact",
+ compactDisplay: "short",
+ }).format(total)
+ })
+
+ const context = createMemo(() => {
+ if (!last()) return
+ if (!model()?.limit.context) return 0
+ const tokens = last().tokens
+ const total = tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
+ return Math.round((total / model()!.limit.context) * 100)
+ })
+
const getScrollParent = (el: HTMLElement | null): HTMLElement | undefined => {
let p = el?.parentElement
while (p && p !== document.body) {
@@ -294,23 +359,6 @@ export default function SessionTimeline(props: { session: string; class?: string
lastScrollY = scroll.y
})
- const valid = (part: Part) => {
- if (!part) return false
- switch (part.type) {
- case "step-start":
- case "step-finish":
- case "file":
- case "patch":
- return false
- case "text":
- return !part.synthetic
- case "reasoning":
- return part.text.trim()
- default:
- return true
- }
- }
-
const duration = (part: Part) => {
switch (part.type) {
default:
@@ -334,57 +382,66 @@ export default function SessionTimeline(props: { session: string; class?: string
-
+
+
+
+
+
+
+ {context()}%
+
+
{cost()}
+
+
+
{(message) => (
-
- {(part) => (
- -
- {part.type}
}>
-
- {(part) => (
-
-
-
-
- {part().text}
-
-
- {DateTime.fromMillis(message.time.created).toRelative()} ยท{" "}
- {sync.data.config.username ?? "user"}
-
-
-
-
-
-
-
- )}
-
-
- {(part) => (
- Thinking}>
-
- Thought for {duration(part())}s
-
-
- }
- >
-
-
- )}
-
-
{(part) => }
-
-
- )}
-
+
+
+ {(part) => (
+
+ {part.type}
}>
+
+ {(part) => (
+
+
+
+
+ {part().text}
+
+
+
+
+
+
+
+ )}
+
+
+ {(part) => (
+ Thinking}>
+
+ Thought for {duration(part())}s
+
+
+ }
+ >
+
+
+ )}
+
+
{(part) => }
+
+
+ )}
+
+
)}
diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx
index 4133887c..80473d84 100644
--- a/packages/desktop/src/pages/index.tsx
+++ b/packages/desktop/src/pages/index.tsx
@@ -238,7 +238,12 @@ export default function Page() {
New Session
-