opencode-glance 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +101 -2
- package/README.zh.md +184 -0
- package/dist/gui-experimental.cjs +99 -0
- package/package.json +66 -4
- package/src/gui/glance.tsx +127 -0
- package/src/gui/styles.ts +104 -0
- package/src/gui.tsx +31 -0
- package/src/i18n/index.ts +233 -0
- package/src/index.ts +11 -0
- package/src/shared/glance.ts +240 -0
- package/src/shared/metrics.ts +262 -0
- package/src/tui.tsx +132 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rosario Weynand
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,102 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-glance
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[中文](./README.zh.md) · English
|
|
4
|
+
|
|
5
|
+
A glanceable status bar for OpenCode v2, in both the terminal and Desktop:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
◷ 7 turns 135 steps · 126 tok/s ▤ 16.2M tok · 94% cached ◔ 18%
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
One package ships a **GUI** and a **TUI** part with the same metrics:
|
|
12
|
+
|
|
13
|
+
| Metric | Definition |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| Turns / steps | User messages / assistant messages in the session (steps can count tool calls instead) |
|
|
16
|
+
| tok/s | `Σ(output + reasoning) / Σ(step end - step start)` over completed steps; live estimation while streaming |
|
|
17
|
+
| Total tokens | `input + output + reasoning + cache.read + cache.write` across the session family |
|
|
18
|
+
| Cache hit | `cache.read / (input + cache.read + cache.write)` |
|
|
19
|
+
| Context | Last assistant step's token total / model `limit.context`, matching OpenCode's built-in indicator |
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
From npm:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
opencode plugin add opencode-glance
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or add it to the global config directly:
|
|
30
|
+
|
|
31
|
+
```jsonc
|
|
32
|
+
// ~/.config/opencode/opencode.jsonc
|
|
33
|
+
{
|
|
34
|
+
"plugins": ["opencode-glance"]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
From a local checkout or GitHub:
|
|
39
|
+
|
|
40
|
+
```jsonc
|
|
41
|
+
{
|
|
42
|
+
"plugins": ["file:///absolute/path/to/opencode-glance"]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The server part is loaded from `opencode.json(c)`, which also makes the TUI part available. TUI-only options must be written in `cli.json` because the server inventory does not carry options for TUI parts:
|
|
47
|
+
|
|
48
|
+
```jsonc
|
|
49
|
+
// ~/.config/opencode/cli.json
|
|
50
|
+
{
|
|
51
|
+
"plugins": [
|
|
52
|
+
{
|
|
53
|
+
"package": "opencode-glance",
|
|
54
|
+
"options": { "locale": "auto", "rate": "auto" }
|
|
55
|
+
},
|
|
56
|
+
"-opencode.sidebar.context"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`-opencode.sidebar.context` hides the built-in Context block in the TUI so the bar becomes its replacement. On Desktop the built-in Context indicator cannot be removed by third-party plugins; the bar is additive.
|
|
62
|
+
|
|
63
|
+
## Options
|
|
64
|
+
|
|
65
|
+
| Option | Default | Description |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `show` | `true` | Show the bar at startup; toggle it from the command palette |
|
|
68
|
+
| `steps` | `"assistant"` | `"assistant"` counts assistant messages; `"tool"` counts tool calls |
|
|
69
|
+
| `rate` | `"auto"` | `"auto"`: live estimate while streaming, last exact step while idle. `"average"`: session average. `"last"`: last exact step |
|
|
70
|
+
| `icons` | `true` | TUI icons (`◷ ▤ ◔`) |
|
|
71
|
+
| `locale` | `"auto"` | TUI locale; `auto` follows the system language. GUI follows the app language |
|
|
72
|
+
|
|
73
|
+
## i18n
|
|
74
|
+
|
|
75
|
+
Locales: `en`, `zh`, `zht`, `ja`, `ko`, `de`, `fr`, `es`, `pt`, `it`. Unsupported locales fall back to English. Turns and steps use `Intl.PluralRules` for correct plurals. Add a new catalog in `src/i18n/index.ts` and append it to `locales`.
|
|
76
|
+
|
|
77
|
+
The GUI follows the OpenCode Desktop/Web language. The TUI uses the system language by default and can be pinned with the `locale` option in `cli.json`.
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install
|
|
83
|
+
npm run typecheck
|
|
84
|
+
npm test # unit tests for the metric functions
|
|
85
|
+
npm run build # build the GUI bundle
|
|
86
|
+
npm run dev # vite build --watch
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`dist/gui-experimental.cjs` is committed so a clone works with `file://` without building. Rebuild it after changing GUI code; CI fails when the committed bundle differs from a fresh build.
|
|
90
|
+
|
|
91
|
+
## Live speed
|
|
92
|
+
|
|
93
|
+
OpenCode writes `tokens` only when a step ends, so an exact per-token rate is not exposed to plugins. `rate: "auto"` estimates tokens from streamed characters (CJK = 1 token, other characters = 1/4) once per second and prefixes the value with `~`. While a tool runs, the bar shows the tool and elapsed time instead of a meaningless speed.
|
|
94
|
+
|
|
95
|
+
## Limitations
|
|
96
|
+
|
|
97
|
+
- The GUI part uses the experimental `./gui-experimental` API and may need updates when OpenCode changes.
|
|
98
|
+
- The TUI bar uses `session.composer.top`. Do not `replace` the whole `sidebar.content` slot; it also holds todo, MCP, and limit blocks.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# opencode-glance
|
|
2
|
+
|
|
3
|
+
中文 · [English](./README.md)
|
|
4
|
+
|
|
5
|
+
一个 OpenCode v2 插件,用一条胶囊状态条展示当前会话的关键指标:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
◷ 7 轮 135 步 · 126 tok/s ▤ 16.2M tok · 缓存命中 94% ◔ 18%
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
同一个包同时提供 **GUI** 和 **TUI** 两个 part,指标口径一致:
|
|
12
|
+
|
|
13
|
+
| 指标 | 口径 |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| 轮 / 步 | 当前会话的 user 消息数 / assistant 消息数(可改为工具调用数) |
|
|
16
|
+
| tok/s | `Σ(output + reasoning) / Σ(step 结束 - step 开始)`,只统计已完成的 step |
|
|
17
|
+
| 总 tok | 当前会话及已知子 agent 会话的 `input + output + reasoning + cache.read + cache.write` |
|
|
18
|
+
| 缓存命中 | `cache.read / (input + cache.read + cache.write)` |
|
|
19
|
+
| 上下文 % | 最近一次 assistant step 的 token 总量 / 模型 `limit.context`,与内置 Context 指示器同一口径 |
|
|
20
|
+
|
|
21
|
+
## 结构
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
src/
|
|
25
|
+
├─ index.ts # 最小 server part,提供插件 id
|
|
26
|
+
├─ tui.tsx # TUI part:session.composer.top 上的圆角状态条
|
|
27
|
+
├─ gui.tsx # GUI part:window.bottom 上的胶囊状态条
|
|
28
|
+
├─ gui/
|
|
29
|
+
│ ├─ glance.tsx # 状态条组件
|
|
30
|
+
│ └─ styles.ts # 注入的 CSS(使用 --v2-* 主题变量)
|
|
31
|
+
├─ shared/
|
|
32
|
+
│ ├─ metrics.ts # 两端共用的纯计算
|
|
33
|
+
│ └─ glance.ts # 两端共用的响应式聚合与文案
|
|
34
|
+
└─ i18n/ # 共用语言包(en / zh / zht / ja / ko / de / fr / es / pt / it)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- TUI part 直接以 TSX 加载,改源码会热重载。
|
|
38
|
+
- GUI part 必须先构建成单个 CommonJS 文件:`dist/gui-experimental.cjs`。
|
|
39
|
+
|
|
40
|
+
## 开发
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install
|
|
44
|
+
npm run typecheck
|
|
45
|
+
npm test # 指标纯函数的单元测试
|
|
46
|
+
npm run build # 构建 GUI
|
|
47
|
+
npm run dev # vite build --watch,改 GUI 代码自动重载
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`dist/gui-experimental.cjs` 已提交到仓库,便于 clone 后直接以 `file://` 方式安装;修改 GUI 源码后记得重新 `npm run build` 并一起提交。CI 会校验提交的 bundle 与重新构建的结果一致。
|
|
51
|
+
|
|
52
|
+
## 安装
|
|
53
|
+
|
|
54
|
+
从 npm 安装:
|
|
55
|
+
|
|
56
|
+
```jsonc
|
|
57
|
+
// ~/.config/opencode/opencode.jsonc
|
|
58
|
+
{
|
|
59
|
+
"plugins": ["opencode-glance"]
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
也可以直接从 GitHub / 本地目录加载。`opencode.json(c)` 是 GUI 读取插件的地方;包里有 server part,所以 TUI part 也会随 server 加载。
|
|
64
|
+
|
|
65
|
+
```jsonc
|
|
66
|
+
{
|
|
67
|
+
"plugins": [
|
|
68
|
+
"file:///absolute/path/to/opencode-glance"
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
需要给 GUI 配置选项时使用对象形式(TUI 选项要写在下面的 `cli.json` 里):
|
|
74
|
+
|
|
75
|
+
```jsonc
|
|
76
|
+
{
|
|
77
|
+
"plugins": [
|
|
78
|
+
{
|
|
79
|
+
"package": "file:///absolute/path/to/opencode-glance",
|
|
80
|
+
"options": {
|
|
81
|
+
"show": true,
|
|
82
|
+
"steps": "assistant",
|
|
83
|
+
"rate": "auto"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### TUI:去掉旧的 Context 块(可选)
|
|
91
|
+
|
|
92
|
+
TUI 的内置 Context 块可以被配置禁用,这样状态条就是真正的替代品。TUI 的插件选项必须写在 `cli.json` 里:通过 server 清单加载的 TUI part 不会携带 `options`。
|
|
93
|
+
|
|
94
|
+
```jsonc
|
|
95
|
+
// ~/.config/opencode/cli.json
|
|
96
|
+
{
|
|
97
|
+
"plugins": [
|
|
98
|
+
{
|
|
99
|
+
"package": "opencode-glance",
|
|
100
|
+
"options": { "locale": "zh", "rate": "auto" }
|
|
101
|
+
},
|
|
102
|
+
"-opencode.sidebar.context"
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
本地目录同样适用:
|
|
108
|
+
|
|
109
|
+
```jsonc
|
|
110
|
+
{
|
|
111
|
+
"plugins": [
|
|
112
|
+
{
|
|
113
|
+
"package": "file:///absolute/path/to/opencode-glance",
|
|
114
|
+
"options": { "locale": "zh", "rate": "auto" }
|
|
115
|
+
},
|
|
116
|
+
"-opencode.sidebar.context"
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### GUI 的限制
|
|
122
|
+
|
|
123
|
+
GUI 的内置 Context 指示器属于 app 内置插件,**第三方 GUI 插件无法移除它**(GUI slot 只支持追加/排序,内置插件在设置里也没有开关)。所以桌面端是“新增状态条 + 旧指示器保留”。如果必须让旧 Context 消失,请使用 TUI。
|
|
124
|
+
|
|
125
|
+
## 命令
|
|
126
|
+
|
|
127
|
+
两个界面都注册了 `Glance: toggle status bar` 命令,可在命令面板里显示/隐藏状态条。
|
|
128
|
+
|
|
129
|
+
## 选项
|
|
130
|
+
|
|
131
|
+
| 选项 | 默认 | 说明 |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| `show` | `true` | 启动时是否显示状态条;之后可用命令切换 |
|
|
134
|
+
| `steps` | `"assistant"` | `"assistant"` 按 assistant 消息计步,`"tool"` 按工具调用计步 |
|
|
135
|
+
| `rate` | `"auto"` | `"auto"` 运行中用流式文本估算实时速度(带 `~`),空闲显示最近一步真实速度;`"average"` 始终用全历史平均;`"last"` 始终用最近一步真实速度 |
|
|
136
|
+
| `icons` | `true` | TUI 是否显示 `◷ ▤ ◔` 图标 |
|
|
137
|
+
| `locale` | `"auto"` | TUI 文案:`"auto"` 跟随系统语言,也可指定 `zh`、`zht`、`en`、`ja`、`ko`、`de`、`fr`、`es`、`pt`、`it`。GUI 跟随 app 语言 |
|
|
138
|
+
|
|
139
|
+
## 国际化
|
|
140
|
+
|
|
141
|
+
- **GUI**:跟随 OpenCode Desktop / Web 的界面语言,由 `Plugin.define({ i18n: messages })` 提供。
|
|
142
|
+
- **TUI**:默认 `locale: "auto"`,按系统语言匹配;也可在 `cli.json` 的 options 里指定。
|
|
143
|
+
- 当前语言包:`en`、`zh`、`zht`、`ja`、`ko`、`de`、`fr`、`es`、`pt`、`it`。
|
|
144
|
+
- 其他语言回退英文。新增语言只需要在 `src/i18n/index.ts` 加一份 `Catalog`,并加入 `locales`。
|
|
145
|
+
- 轮/步使用了 `Intl.PluralRules`,英文、法文、德文等单复数形式正确。
|
|
146
|
+
|
|
147
|
+
## 指标细节
|
|
148
|
+
|
|
149
|
+
- `steps` 默认按 OpenCode `SessionStats` 的口径统计 assistant 消息;设置 `"steps": "tool"` 可改为统计工具调用数。
|
|
150
|
+
- `tok/s` 的严格实时值拿不到:OpenCode 只在 step 结束时写入 `tokens`。`rate: "auto"` 在运行中用已流出的文本估算 token(CJK 按字、其他字符按 1/4),每秒刷新并带 `~` 前缀;空闲时回落到最近一步的真实速度。运行工具时显示工具名和已用时,而不是速度。
|
|
151
|
+
- 运行中的状态条会优先显示当前动作:工具执行显示 `bash 8s`,等待首 token 显示 `等待响应 3s`,流式生成显示 `~131 tok/s · 12s`。
|
|
152
|
+
- 速度分母是 `time.streamed - time.created`,包含首 token 等待时间和请求分发,因此是端到端生成速度,不是纯解码速度。
|
|
153
|
+
- 总 token 和缓存命中按 session family 汇总(根会话 + 已同步的子 agent),与内置 cost 的 family 口径一致。
|
|
154
|
+
- 上下文百分比只取根会话最近一次有效 assistant step,且跳过已压缩内容和 undo 边界之后的消息,与内置 Context 面板一致。
|
|
155
|
+
- 无数据时显示 `—`,不会出现 NaN。
|
|
156
|
+
|
|
157
|
+
## 发布
|
|
158
|
+
|
|
159
|
+
OpenCode 官方没有集中式插件市场,分发路径是:
|
|
160
|
+
|
|
161
|
+
1. **npm**:`npm publish --access public`,用户用 `opencode plugin add opencode-glance`。
|
|
162
|
+
2. **opencode.im**:发布到 npm 后,它会按 `opencode-plugin` keyword 自动发现;也可以手动提交。
|
|
163
|
+
3. **官方 Ecosystem 列表**:向 `anomalyco/opencode` 的 `packages/web/src/content/docs/ecosystem.mdx` 提交 PR。
|
|
164
|
+
4. **awesome-opencode**:向社区列表提交 PR。
|
|
165
|
+
|
|
166
|
+
本仓库已包含 `opencode-plugin` keyword、MIT License 和 CI。发布前:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npm login
|
|
170
|
+
npm publish --access public
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
也可以用 tag 触发 `.github/workflows/release.yml`:先配置 `NPM_TOKEN` secret,然后 `git tag v0.1.0 && git push --tags`。
|
|
174
|
+
|
|
175
|
+
`prepublishOnly` 会先跑 `typecheck` 和 GUI 构建。
|
|
176
|
+
|
|
177
|
+
## 许可证
|
|
178
|
+
|
|
179
|
+
MIT。见 [LICENSE](./LICENSE)。
|
|
180
|
+
|
|
181
|
+
## 注意
|
|
182
|
+
|
|
183
|
+
- GUI part 使用 experimental API(`./gui-experimental`),OpenCode 小版本升级可能需要跟进。
|
|
184
|
+
- TUI 状态条依赖 `session.composer.top` slot;不要把 `sidebar.content` 整体 `replace`,否则会连带移除内置 todo、MCP、limit 块。
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const l=require("solid-js/web"),G=require("@opencode/plugin/gui"),s=require("solid-js"),z={input:0,output:0,reasoning:0,cache:{read:0,write:0}};function A(t,e){return e?{input:t.input+e.input,output:t.output+e.output,reasoning:t.reasoning+e.reasoning,cache:{read:t.cache.read+e.cache.read,write:t.cache.write+e.cache.write}}:t}function _(t){return t.input+t.output+t.reasoning+t.cache.read+t.cache.write}function q(t){return t.reduce((e,n)=>e+(n.type==="user"?1:0),0)}function B(t,e="assistant"){return e==="tool"?t.reduce((n,r)=>r.type!=="assistant"?n:n+r.content.reduce((a,o)=>a+(o.type==="tool"?1:0),0),0):t.reduce((n,r)=>n+(r.type==="assistant"?1:0),0)}function K(t){let e=0,n=0;for(const r of t){if(r.type!=="assistant"||!r.tokens||typeof r.time.streamed!="number")continue;const a=r.tokens.output+r.tokens.reasoning;if(a<=0)continue;const o=(r.time.streamed-r.time.created)/1e3;o<=0||(e+=a,n+=o)}return n>0?Math.round(e/n):void 0}function P(t){for(let e=t.length-1;e>=0;e--){const n=t[e];if(!n||n.type!=="assistant"||!n.tokens||typeof n.time.streamed!="number")continue;const r=n.tokens.output+n.tokens.reasoning;if(r<=0)continue;const a=(n.time.streamed-n.time.created)/1e3;if(!(a<=0))return Math.round(r/a)}}function R(t){return t.findLast(e=>e.type==="assistant"&&e.time.completed===void 0)}const F=/[\u2e80-\u2eff\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;function J(t){let e=0,n=0;for(const r of t)/\s/.test(r)||(F.test(r)?e++:n++);return Math.ceil(e+n/4)}function O(t){let e=0;for(const n of t.content){if(n.type==="text"||n.type==="reasoning"){e+=J(n.text);continue}if(n.type!=="tool")continue;const r=n.state.status==="streaming"?n.state.input:JSON.stringify(n.state.input);e+=Math.ceil((r?.length??0)/4)}return e}function U(t){return t.content.findLast(e=>e.type==="tool"&&e.state.status==="running")}function V(t,e,n){if(!t)return;const r=Math.max(0,(n-t.time.created)/1e3),a=U(t);if(a){const o=a.time.ran??a.time.created;return{kind:"tool",name:a.name,elapsed:Math.max(0,(n-o)/1e3)}}return e>0?{kind:"streaming",elapsed:r,estimatedTokens:e,rate:r>=2?Math.round(e/r):void 0}:{kind:"waiting",elapsed:r}}function C(t){const e=Math.max(0,Math.floor(t));if(e<60)return`${e}s`;const n=Math.floor(e/60);if(n<60){const o=e%60;return o?`${n}m ${o}s`:`${n}m`}const r=Math.floor(n/60),a=n%60;return a?`${r}h ${a}m`:`${r}h`}function H(t){const e=t.input+t.cache.read+t.cache.write;if(!(e<=0))return Math.max(0,Math.min(100,Math.round(t.cache.read/e*100)))}function Z(t,e){const n=e?t.findIndex(o=>o.id===e):-1;if(e&&n===-1)return;const r=n===-1?t.length:n,a=t.findLastIndex((o,i)=>o.type==="compaction"&&o.status==="completed"&&i<r);return t.findLast((o,i)=>o.type==="assistant"&&o.tokens!==void 0&&i>a&&i<r)}function W(t,e,n){const r=Z(t,n);if(!r)return;const a=_(r.tokens);if(a<=0)return;const o=e?.find(i=>i.providerID===r.model.providerID&&i.id===r.model.id);return{tokens:a,percent:o?.limit.context?Math.max(0,Math.min(100,Math.round(a/o.limit.context*100))):void 0}}function Y(t){if(!Number.isFinite(t)||t<0)return String(t);const e=[[1e3,"K"],[1e6,"M"],[1e9,"B"],[1e12,"T"]];let n=-1;for(let a=0;a<e.length;a++)t>=e[a][0]&&(n=a);if(n===-1)return String(t);let r=Number((t/e[n][0]).toFixed(1));return r>=1e3&&n+1<e.length&&(n++,r=Number((t/e[n][0]).toFixed(1))),`${r}${e[n][1]}`}function Q(t){return t==="average"||t==="last"||t==="auto"?t:"auto"}function X(t){return t==="tool"?"tool":"assistant"}function ee(t,e=18){return t.length>e?`${t.slice(0,e-1)}…`:t}function te(t){const e=t[t.length-1];return e?e.type==="assistant"?[t.length,e.id,"a",e.time.completed??0,e.time.streamed??0,e.tokens?.input??0,e.tokens?.output??0,e.tokens?.cache.read??0,e.tokens?.cache.write??0].join("|"):e.type==="compaction"?`${t.length}|${e.id}|c|${e.status}`:`${t.length}|${e.id}|${e.type}`:"0"}const ne=t=>t!==void 0;function ae(t,e){const n=te(e);return t&&t.key===n?t:{key:n,messages:e}}function re(t,e){const[n,r]=s.createSignal(Date.now()),[a,o]=s.createSignal();s.createEffect(()=>{if(!t.running())return;r(Date.now());const c=setInterval(()=>r(Date.now()),1e3);s.onCleanup(()=>clearInterval(c))});const i=s.createMemo(c=>ae(c,t.messages())),$=s.createMemo(()=>t.family()),h=s.createMemo(()=>$().reduce((c,u)=>A(c,t.sessionTokens(u)),z)),k=s.createMemo(()=>W(i().messages,t.models(),t.session()?.revert?.messageID)),m=s.createMemo(()=>q(i().messages)),x=s.createMemo(()=>B(i().messages,e.steps)),d=s.createMemo(()=>K(i().messages)),v=s.createMemo(()=>P(i().messages)),g=s.createMemo(()=>H(h())),p=s.createMemo(()=>t.running()),f=s.createMemo(()=>{const c=R(t.messages());return V(c,c?O(c):0,n())});s.createEffect(()=>{const c=f();if(!p()||!c||c.kind!=="streaming"||c.rate===void 0){o(void 0);return}const u=c.rate;o(M=>M===void 0?u:Math.round(M*.65+u*.35))});const E=s.createMemo(()=>e.rate==="average"?d():v()??d()),w=s.createMemo(()=>`${e.plural("glance.turn",m())} ${e.plural("glance.step",x())}`),b=s.createMemo(()=>e.t("glance.tokens",{tokens:Y(_(h()))})),T=s.createMemo(()=>{const c=g();return c===void 0?void 0:e.t("glance.cache",{percent:c})}),S=s.createMemo(()=>{const c=k()?.percent;return c===void 0?"—":e.t("glance.context",{percent:c})}),y=s.createMemo(()=>{const c=f();if(!p()){const I=E();return I===void 0?void 0:e.t("glance.rate",{rate:I})}if(!c)return e.t("glance.preparing");if(c.kind==="tool")return e.t("glance.tool",{name:ee(c.name),elapsed:C(c.elapsed)});if(c.kind==="waiting")return e.t("glance.waiting",{elapsed:C(c.elapsed)});const u=C(c.elapsed);if(e.rate==="auto"&&a()!==void 0)return e.t("glance.live",{rate:a(),elapsed:u});const M=e.rate==="average"?d():v()??d();return M===void 0?e.t("glance.streaming",{elapsed:u}):e.t("glance.rateElapsed",{rate:M,elapsed:u})}),j=s.createMemo(()=>y()?`${w()} · ${y()}`:w()),D=s.createMemo(()=>T()?`${b()} · ${T()}`:b()),N=s.createMemo(()=>`${m()}/${x()} · ${S()}`),L=s.createMemo(()=>(p()?[w(),y(),b(),S()]:[w(),y(),b(),T(),S()]).filter(ne).join(" · "));return{totals:h,hit:g,usage:k,turns:m,steps:x,running:p,speed:E,activity:f,turnText:w,tokensText:b,cacheText:T,contextText:S,activityText:y,progressText:j,usageText:D,compactText:N,mediumText:L}}var oe=l.template('<svg width=14 height=14 viewBox="0 0 16 16"fill=none stroke=currentColor stroke-width=1.4 stroke-linecap=round stroke-linejoin=round aria-hidden=true><circle cx=8 cy=8 r=6></circle><path d="M8 4.5V8l2.4 1.5">'),ce=l.template('<svg width=14 height=14 viewBox="0 0 16 16"fill=none stroke=currentColor stroke-width=1.4 stroke-linecap=round stroke-linejoin=round aria-hidden=true><ellipse cx=8 cy=4.3 rx=5 ry=2.1></ellipse><path d="M3 4.3v7.4c0 1.2 2.2 2.1 5 2.1s5-.9 5-2.1V4.3"></path><path d="M3 8c0 1.2 2.2 2.1 5 2.1s5-.9 5-2.1">'),se=l.template("<span class=glance-ring aria-hidden=true>"),le=l.template('<div class=glance-root><div class=glance-bar role=status><span class=glance-group><span class=glance-icon></span><span class=glance-label></span></span><span class="glance-group center"><span class=glance-icon></span><span class=glance-label></span></span><span class="glance-group end"><span class=glance-label>');function ie(){return oe()}function ge(){return ce()}function ue(t){const e=()=>Math.max(0,Math.min(100,t.percent??0))*3.6;return(()=>{var n=se();return l.effect(r=>l.setStyleProperty(n,"background",`conic-gradient(currentColor ${e()}deg, var(--v2-background-bg-layer-04) 0)`)),n})()}function de(t){const e=t.ctx,n=()=>e.sessions.current(),r=re({messages:()=>{const a=n();return a?.id?a.server.data.session.message.list(a.id):[]},session:()=>{const a=n();return a?.id?a.server.data.session.get(a.id):void 0},family:()=>{const a=n();if(!a?.id)return[];const o=a.server.data.session.family(a.id);return o.length?o:[a.id]},sessionTokens:a=>n()?.server.data.session.get(a)?.tokens,models:()=>{const a=n();return a?a.server.data.location.model.list(a.location):void 0},running:()=>{const a=n();return a?.id?a.server.data.session.status(a.id)==="running":!1}},{steps:X(e.options.steps),rate:Q(e.options.rate),t:(a,o)=>e.t(a,o),plural:(a,o)=>e.plural(a,o)});return l.createComponent(s.Show,{get when(){return n()},get children(){var a=le(),o=a.firstChild,i=o.firstChild,$=i.firstChild,h=$.nextSibling,k=i.nextSibling,m=k.firstChild,x=m.nextSibling,d=k.nextSibling,v=d.firstChild;return l.insert($,l.createComponent(ie,{})),l.insert(h,()=>r.progressText()),l.insert(m,l.createComponent(ge,{})),l.insert(x,()=>r.usageText()),l.insert(d,l.createComponent(ue,{get percent(){return r.usage()?.percent}}),v),l.insert(v,()=>r.contextText()),l.effect(g=>{var p=`${r.progressText()} · ${r.usageText()} · ${r.contextText()}`,f=`${_(r.totals()).toLocaleString()} tokens · ${(r.usage()?.tokens??0).toLocaleString()} in context`;return p!==g.e&&l.setAttribute(o,"aria-label",g.e=p),f!==g.t&&l.setAttribute(o,"title",g.t=f),g},{e:void 0,t:void 0}),a}})}const pe=`
|
|
2
|
+
.glance-root {
|
|
3
|
+
display: flex;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
padding: 4px 12px 8px;
|
|
6
|
+
pointer-events: none;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.glance-bar {
|
|
10
|
+
pointer-events: auto;
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: space-between;
|
|
14
|
+
gap: 16px;
|
|
15
|
+
width: 100%;
|
|
16
|
+
max-width: 1240px;
|
|
17
|
+
height: 36px;
|
|
18
|
+
padding: 0 18px;
|
|
19
|
+
border-radius: 999px;
|
|
20
|
+
border: 0.5px solid var(--v2-border-border-muted);
|
|
21
|
+
background: var(--v2-background-bg-base);
|
|
22
|
+
box-shadow: var(--v2-elevation-raised);
|
|
23
|
+
color: var(--v2-text-text-base);
|
|
24
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
25
|
+
font-size: 13px;
|
|
26
|
+
font-weight: 500;
|
|
27
|
+
line-height: 1;
|
|
28
|
+
letter-spacing: -0.04px;
|
|
29
|
+
white-space: nowrap;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.glance-group {
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 7px;
|
|
37
|
+
min-width: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.glance-group.center {
|
|
41
|
+
flex: 1;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.glance-group.end {
|
|
46
|
+
justify-content: flex-end;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.glance-icon {
|
|
50
|
+
display: inline-flex;
|
|
51
|
+
flex: none;
|
|
52
|
+
color: var(--v2-text-text-muted);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.glance-label {
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
text-overflow: ellipsis;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.glance-muted {
|
|
61
|
+
color: var(--v2-text-text-muted);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.glance-ring {
|
|
65
|
+
position: relative;
|
|
66
|
+
display: inline-block;
|
|
67
|
+
flex: none;
|
|
68
|
+
width: 16px;
|
|
69
|
+
height: 16px;
|
|
70
|
+
border-radius: 50%;
|
|
71
|
+
color: var(--v2-icon-icon-base, var(--v2-text-text-muted));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.glance-ring::after {
|
|
75
|
+
content: "";
|
|
76
|
+
position: absolute;
|
|
77
|
+
inset: 3px;
|
|
78
|
+
border-radius: 50%;
|
|
79
|
+
background: var(--v2-background-bg-base);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@media (max-width: 760px) {
|
|
83
|
+
.glance-group.center {
|
|
84
|
+
display: none;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@media (max-width: 480px) {
|
|
89
|
+
.glance-root {
|
|
90
|
+
padding: 2px 8px 6px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.glance-bar {
|
|
94
|
+
height: 30px;
|
|
95
|
+
padding: 0 12px;
|
|
96
|
+
font-size: 12px;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
`,me={"glance.turn.one":"{{count}} turn","glance.turn.other":"{{count}} turns","glance.step.one":"{{count}} step","glance.step.other":"{{count}} steps","glance.rate":"{{rate}} tok/s","glance.rateElapsed":"{{rate}} tok/s · {{elapsed}}","glance.live":"~{{rate}} tok/s · {{elapsed}}","glance.tokens":"{{tokens}} tok","glance.cache":"cache hit {{percent}}%","glance.context":"{{percent}}%","glance.tool":"{{name}} {{elapsed}}","glance.waiting":"waiting {{elapsed}}","glance.streaming":"generating {{elapsed}}","glance.preparing":"preparing","glance.command.toggle":"Glance: toggle status bar","glance.command.group":"Glance"},fe={"glance.turn.one":"{{count}} 轮","glance.turn.other":"{{count}} 轮","glance.step.one":"{{count}} 步","glance.step.other":"{{count}} 步","glance.rate":"{{rate}} tok/s","glance.rateElapsed":"{{rate}} tok/s · {{elapsed}}","glance.live":"~{{rate}} tok/s · {{elapsed}}","glance.tokens":"{{tokens}} tok","glance.cache":"缓存命中 {{percent}}%","glance.context":"{{percent}}%","glance.tool":"{{name}} {{elapsed}}","glance.waiting":"等待响应 {{elapsed}}","glance.streaming":"生成中 {{elapsed}}","glance.preparing":"准备中","glance.command.toggle":"Glance:显示/隐藏状态条","glance.command.group":"Glance"},he={"glance.turn.one":"{{count}} 輪","glance.turn.other":"{{count}} 輪","glance.step.one":"{{count}} 步","glance.step.other":"{{count}} 步","glance.rate":"{{rate}} tok/s","glance.rateElapsed":"{{rate}} tok/s · {{elapsed}}","glance.live":"~{{rate}} tok/s · {{elapsed}}","glance.tokens":"{{tokens}} tok","glance.cache":"快取命中 {{percent}}%","glance.context":"{{percent}}%","glance.tool":"{{name}} {{elapsed}}","glance.waiting":"等待回應 {{elapsed}}","glance.streaming":"生成中 {{elapsed}}","glance.preparing":"準備中","glance.command.toggle":"Glance:顯示/隱藏狀態列","glance.command.group":"Glance"},ke={"glance.turn.one":"{{count}} ターン","glance.turn.other":"{{count}} ターン","glance.step.one":"{{count}} ステップ","glance.step.other":"{{count}} ステップ","glance.rate":"{{rate}} tok/s","glance.rateElapsed":"{{rate}} tok/s · {{elapsed}}","glance.live":"~{{rate}} tok/s · {{elapsed}}","glance.tokens":"{{tokens}} tok","glance.cache":"キャッシュヒット {{percent}}%","glance.context":"{{percent}}%","glance.tool":"{{name}} {{elapsed}}","glance.waiting":"応答待ち {{elapsed}}","glance.streaming":"生成中 {{elapsed}}","glance.preparing":"準備中","glance.command.toggle":"Glance:ステータスバーを表示/非表示","glance.command.group":"Glance"},xe={"glance.turn.one":"{{count}}턴","glance.turn.other":"{{count}}턴","glance.step.one":"{{count}}단계","glance.step.other":"{{count}}단계","glance.rate":"{{rate}} tok/s","glance.rateElapsed":"{{rate}} tok/s · {{elapsed}}","glance.live":"~{{rate}} tok/s · {{elapsed}}","glance.tokens":"{{tokens}} tok","glance.cache":"캐시 적중 {{percent}}%","glance.context":"{{percent}}%","glance.tool":"{{name}} {{elapsed}}","glance.waiting":"응답 대기 {{elapsed}}","glance.streaming":"생성 중 {{elapsed}}","glance.preparing":"준비 중","glance.command.toggle":"Glance: 상태 표시줄 표시/숨기기","glance.command.group":"Glance"},ve={"glance.turn.one":"{{count}} Zug","glance.turn.other":"{{count}} Züge","glance.step.one":"{{count}} Schritt","glance.step.other":"{{count}} Schritte","glance.rate":"{{rate}} tok/s","glance.rateElapsed":"{{rate}} tok/s · {{elapsed}}","glance.live":"~{{rate}} tok/s · {{elapsed}}","glance.tokens":"{{tokens}} tok","glance.cache":"Cache-Treffer {{percent}}%","glance.context":"{{percent}}%","glance.tool":"{{name}} {{elapsed}}","glance.waiting":"warte auf Antwort {{elapsed}}","glance.streaming":"generiert {{elapsed}}","glance.preparing":"bereit","glance.command.toggle":"Glance: Statusleiste ein-/ausblenden","glance.command.group":"Glance"},we={"glance.turn.one":"{{count}} tour","glance.turn.other":"{{count}} tours","glance.step.one":"{{count}} étape","glance.step.other":"{{count}} étapes","glance.rate":"{{rate}} tok/s","glance.rateElapsed":"{{rate}} tok/s · {{elapsed}}","glance.live":"~{{rate}} tok/s · {{elapsed}}","glance.tokens":"{{tokens}} tok","glance.cache":"cache {{percent}}%","glance.context":"{{percent}}%","glance.tool":"{{name}} {{elapsed}}","glance.waiting":"en attente de réponse {{elapsed}}","glance.streaming":"génération {{elapsed}}","glance.preparing":"préparation","glance.command.toggle":"Glance : afficher/masquer la barre d’état","glance.command.group":"Glance"},be={"glance.turn.one":"{{count}} turno","glance.turn.other":"{{count}} turnos","glance.step.one":"{{count}} paso","glance.step.other":"{{count}} pasos","glance.rate":"{{rate}} tok/s","glance.rateElapsed":"{{rate}} tok/s · {{elapsed}}","glance.live":"~{{rate}} tok/s · {{elapsed}}","glance.tokens":"{{tokens}} tok","glance.cache":"aciertos de caché {{percent}}%","glance.context":"{{percent}}%","glance.tool":"{{name}} {{elapsed}}","glance.waiting":"esperando respuesta {{elapsed}}","glance.streaming":"generando {{elapsed}}","glance.preparing":"preparando","glance.command.toggle":"Glance: mostrar/ocultar la barra de estado","glance.command.group":"Glance"},ye={"glance.turn.one":"{{count}} turno","glance.turn.other":"{{count}} turnos","glance.step.one":"{{count}} passo","glance.step.other":"{{count}} passos","glance.rate":"{{rate}} tok/s","glance.rateElapsed":"{{rate}} tok/s · {{elapsed}}","glance.live":"~{{rate}} tok/s · {{elapsed}}","glance.tokens":"{{tokens}} tok","glance.cache":"acertos de cache {{percent}}%","glance.context":"{{percent}}%","glance.tool":"{{name}} {{elapsed}}","glance.waiting":"aguardando resposta {{elapsed}}","glance.streaming":"gerando {{elapsed}}","glance.preparing":"preparando","glance.command.toggle":"Glance: mostrar/ocultar a barra de status","glance.command.group":"Glance"},Me={"glance.turn.one":"{{count}} turno","glance.turn.other":"{{count}} turni","glance.step.one":"{{count}} passo","glance.step.other":"{{count}} passi","glance.rate":"{{rate}} tok/s","glance.rateElapsed":"{{rate}} tok/s · {{elapsed}}","glance.live":"~{{rate}} tok/s · {{elapsed}}","glance.tokens":"{{tokens}} tok","glance.cache":"hit cache {{percent}}%","glance.context":"{{percent}}%","glance.tool":"{{name}} {{elapsed}}","glance.waiting":"in attesa di risposta {{elapsed}}","glance.streaming":"generazione {{elapsed}}","glance.preparing":"preparazione","glance.command.toggle":"Glance: mostra/nascondi la barra di stato","glance.command.group":"Glance"},$e={en:me,zh:fe,zht:he,ja:ke,ko:xe,de:ve,fr:we,es:be,pt:ye,it:Me},Te=G.Plugin.define({i18n:$e,setup(t){const[e,n]=s.createSignal(t.options.show!==!1),r={at:"window.bottom",order:0,render:()=>l.createComponent(de,{ctx:t})};t.add(G.Style,pe),t.add(G.Slot,()=>e()?r:void 0),t.add(G.Command,()=>({id:"toggle",title:t.t("glance.command.toggle"),group:t.t("glance.command.group"),run:()=>{n(a=>!a)}}))}});exports.default=Te;
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
2
3
|
"name": "opencode-glance",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
4
|
+
"version": "0.1.2",
|
|
5
|
+
"description": "Glanceable session stats for OpenCode: turns, steps, throughput, tokens, cache hit, context usage, and live tool activity.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Rosario Weynand",
|
|
9
|
+
"homepage": "https://github.com/penn0312/opencode-glance#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/penn0312/opencode-glance.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/penn0312/opencode-glance/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"opencode",
|
|
19
|
+
"opencode-plugin",
|
|
20
|
+
"statusline",
|
|
21
|
+
"tokens",
|
|
22
|
+
"cache",
|
|
23
|
+
"context",
|
|
24
|
+
"tui",
|
|
25
|
+
"desktop"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.ts",
|
|
29
|
+
"./tui": "./src/tui.tsx",
|
|
30
|
+
"./gui-experimental": "./dist/gui-experimental.cjs"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"README.zh.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "vite build",
|
|
41
|
+
"dev": "vite build --watch",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@opencode/plugin": "^2.2.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@opentui/core": ">=0.7.0",
|
|
54
|
+
"@opentui/solid": ">=0.7.0",
|
|
55
|
+
"solid-js": ">=1.9.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@opencode/client": "^2.2.0",
|
|
59
|
+
"@opentui/core": "0.7.0",
|
|
60
|
+
"@opentui/solid": "0.7.0",
|
|
61
|
+
"@types/node": "^22.0.0",
|
|
62
|
+
"solid-js": "^1.9.0",
|
|
63
|
+
"typescript": "^5.7.0",
|
|
64
|
+
"vite": "^7.0.0",
|
|
65
|
+
"vite-plugin-solid": "^2.11.0",
|
|
66
|
+
"vitest": "^3.2.7"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Show } from "solid-js"
|
|
2
|
+
import type { Context } from "@opencode/plugin/gui"
|
|
3
|
+
import { createGlance, resolveRate, resolveSteps } from "../shared/glance"
|
|
4
|
+
import { totalTokens } from "../shared/metrics"
|
|
5
|
+
|
|
6
|
+
function ClockIcon() {
|
|
7
|
+
return (
|
|
8
|
+
<svg
|
|
9
|
+
width="14"
|
|
10
|
+
height="14"
|
|
11
|
+
viewBox="0 0 16 16"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
stroke-width="1.4"
|
|
15
|
+
stroke-linecap="round"
|
|
16
|
+
stroke-linejoin="round"
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
>
|
|
19
|
+
<circle cx="8" cy="8" r="6" />
|
|
20
|
+
<path d="M8 4.5V8l2.4 1.5" />
|
|
21
|
+
</svg>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function DatabaseIcon() {
|
|
26
|
+
return (
|
|
27
|
+
<svg
|
|
28
|
+
width="14"
|
|
29
|
+
height="14"
|
|
30
|
+
viewBox="0 0 16 16"
|
|
31
|
+
fill="none"
|
|
32
|
+
stroke="currentColor"
|
|
33
|
+
stroke-width="1.4"
|
|
34
|
+
stroke-linecap="round"
|
|
35
|
+
stroke-linejoin="round"
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
>
|
|
38
|
+
<ellipse cx="8" cy="4.3" rx="5" ry="2.1" />
|
|
39
|
+
<path d="M3 4.3v7.4c0 1.2 2.2 2.1 5 2.1s5-.9 5-2.1V4.3" />
|
|
40
|
+
<path d="M3 8c0 1.2 2.2 2.1 5 2.1s5-.9 5-2.1" />
|
|
41
|
+
</svg>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Ring(props: { percent?: number }) {
|
|
46
|
+
const degree = () => Math.max(0, Math.min(100, props.percent ?? 0)) * 3.6
|
|
47
|
+
return (
|
|
48
|
+
<span
|
|
49
|
+
class="glance-ring"
|
|
50
|
+
style={{
|
|
51
|
+
background: `conic-gradient(currentColor ${degree()}deg, var(--v2-background-bg-layer-04) 0)`,
|
|
52
|
+
}}
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function GlanceBar(props: { ctx: Context }) {
|
|
59
|
+
const ctx = props.ctx
|
|
60
|
+
const session = () => ctx.sessions.current()
|
|
61
|
+
|
|
62
|
+
const glance = createGlance(
|
|
63
|
+
{
|
|
64
|
+
messages: () => {
|
|
65
|
+
const current = session()
|
|
66
|
+
return current?.id ? current.server.data.session.message.list(current.id) : []
|
|
67
|
+
},
|
|
68
|
+
session: () => {
|
|
69
|
+
const current = session()
|
|
70
|
+
return current?.id ? current.server.data.session.get(current.id) : undefined
|
|
71
|
+
},
|
|
72
|
+
family: () => {
|
|
73
|
+
const current = session()
|
|
74
|
+
if (!current?.id) return []
|
|
75
|
+
const ids = current.server.data.session.family(current.id)
|
|
76
|
+
return ids.length ? ids : [current.id]
|
|
77
|
+
},
|
|
78
|
+
sessionTokens: (id) => session()?.server.data.session.get(id)?.tokens,
|
|
79
|
+
models: () => {
|
|
80
|
+
const current = session()
|
|
81
|
+
return current ? current.server.data.location.model.list(current.location) : undefined
|
|
82
|
+
},
|
|
83
|
+
running: () => {
|
|
84
|
+
const current = session()
|
|
85
|
+
return current?.id ? current.server.data.session.status(current.id) === "running" : false
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
steps: resolveSteps(ctx.options.steps),
|
|
90
|
+
rate: resolveRate(ctx.options.rate),
|
|
91
|
+
t: (key, params) => ctx.t(key, params),
|
|
92
|
+
plural: (base, count) => ctx.plural(base, count),
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Show when={session()}>
|
|
98
|
+
<div class="glance-root">
|
|
99
|
+
<div
|
|
100
|
+
class="glance-bar"
|
|
101
|
+
role="status"
|
|
102
|
+
aria-label={`${glance.progressText()} · ${glance.usageText()} · ${glance.contextText()}`}
|
|
103
|
+
title={`${totalTokens(glance.totals()).toLocaleString()} tokens · ${(
|
|
104
|
+
glance.usage()?.tokens ?? 0
|
|
105
|
+
).toLocaleString()} in context`}
|
|
106
|
+
>
|
|
107
|
+
<span class="glance-group">
|
|
108
|
+
<span class="glance-icon">
|
|
109
|
+
<ClockIcon />
|
|
110
|
+
</span>
|
|
111
|
+
<span class="glance-label">{glance.progressText()}</span>
|
|
112
|
+
</span>
|
|
113
|
+
<span class="glance-group center">
|
|
114
|
+
<span class="glance-icon">
|
|
115
|
+
<DatabaseIcon />
|
|
116
|
+
</span>
|
|
117
|
+
<span class="glance-label">{glance.usageText()}</span>
|
|
118
|
+
</span>
|
|
119
|
+
<span class="glance-group end">
|
|
120
|
+
<Ring percent={glance.usage()?.percent} />
|
|
121
|
+
<span class="glance-label">{glance.contextText()}</span>
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</Show>
|
|
126
|
+
)
|
|
127
|
+
}
|