neoctl 0.1.19 → 0.1.21
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/dist/agents/local-agent-task.js +2 -1
- package/dist/agents/local-agent-task.js.map +1 -1
- package/dist/agents/smoke-agents.js +21 -4
- package/dist/agents/smoke-agents.js.map +1 -1
- package/dist/context/prompts.js +4 -0
- package/dist/context/prompts.js.map +1 -1
- package/dist/core/image-storage.d.ts +6 -0
- package/dist/core/image-storage.js +38 -0
- package/dist/core/image-storage.js.map +1 -0
- package/dist/core/query-engine.d.ts +21 -1
- package/dist/core/query-engine.js +103 -13
- package/dist/core/query-engine.js.map +1 -1
- package/dist/core/query.d.ts +2 -1
- package/dist/core/query.js +60 -5
- package/dist/core/query.js.map +1 -1
- package/dist/core/smoke-core-loop.js +95 -6
- package/dist/core/smoke-core-loop.js.map +1 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/model/communication-logger.d.ts +2 -1
- package/dist/model/communication-logger.js +3 -0
- package/dist/model/communication-logger.js.map +1 -1
- package/dist/model/config.d.ts +10 -4
- package/dist/model/config.js +61 -12
- package/dist/model/config.js.map +1 -1
- package/dist/model/context-window.js +1 -0
- package/dist/model/context-window.js.map +1 -1
- package/dist/model/deepseek-adapter.d.ts +29 -0
- package/dist/model/deepseek-adapter.js +108 -0
- package/dist/model/deepseek-adapter.js.map +1 -0
- package/dist/model/env.js +35 -19
- package/dist/model/env.js.map +1 -1
- package/dist/model/kimi-adapter.d.ts +29 -0
- package/dist/model/kimi-adapter.js +108 -0
- package/dist/model/kimi-adapter.js.map +1 -0
- package/dist/model/model-metadata.json +726 -677
- package/dist/model/openai-adapter.d.ts +1 -1
- package/dist/model/openai-chat-mapper.d.ts +4 -1
- package/dist/model/openai-chat-mapper.js +30 -8
- package/dist/model/openai-chat-mapper.js.map +1 -1
- package/dist/model/openai-mappers.d.ts +5 -2
- package/dist/model/openai-mappers.js +33 -6
- package/dist/model/openai-mappers.js.map +1 -1
- package/dist/model/openai-responses-mapper.d.ts +1 -1
- package/dist/model/openai-responses-mapper.js +2 -1
- package/dist/model/openai-responses-mapper.js.map +1 -1
- package/dist/model/provider-factory.js +32 -0
- package/dist/model/provider-factory.js.map +1 -1
- package/dist/model/smoke-deepseek-mapper.d.ts +1 -0
- package/dist/model/smoke-deepseek-mapper.js +65 -0
- package/dist/model/smoke-deepseek-mapper.js.map +1 -0
- package/dist/model/smoke-openai.js +1 -1
- package/dist/model/smoke-openai.js.map +1 -1
- package/dist/model/smoke-responses-mapper.js +6 -6
- package/dist/model/smoke-responses-mapper.js.map +1 -1
- package/dist/open-directory.d.ts +1 -0
- package/dist/open-directory.js +26 -0
- package/dist/open-directory.js.map +1 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +12 -0
- package/dist/paths.js.map +1 -0
- package/dist/repl/commands.d.ts +15 -0
- package/dist/repl/commands.js +58 -0
- package/dist/repl/commands.js.map +1 -1
- package/dist/repl/index.js +1012 -171
- package/dist/repl/index.js.map +1 -1
- package/dist/session/session-export.d.ts +33 -0
- package/dist/session/session-export.js +351 -0
- package/dist/session/session-export.js.map +1 -0
- package/dist/session/session-store.js +2 -2
- package/dist/session/session-store.js.map +1 -1
- package/dist/session/simple-session-runtime.d.ts +74 -0
- package/dist/session/simple-session-runtime.js +171 -0
- package/dist/session/simple-session-runtime.js.map +1 -0
- package/dist/session/smoke-session.js +22 -1
- package/dist/session/smoke-session.js.map +1 -1
- package/dist/skills/skill-filesystem.d.ts +32 -0
- package/dist/skills/skill-filesystem.js +371 -0
- package/dist/skills/skill-filesystem.js.map +1 -0
- package/dist/skills/skill-management-tools.d.ts +36 -0
- package/dist/skills/skill-management-tools.js +188 -0
- package/dist/skills/skill-management-tools.js.map +1 -0
- package/dist/skills/skill-tool.d.ts +85 -5
- package/dist/skills/skill-tool.js +173 -14
- package/dist/skills/skill-tool.js.map +1 -1
- package/dist/skills/smoke-skills.js +54 -5
- package/dist/skills/smoke-skills.js.map +1 -1
- package/dist/tips.d.ts +10 -0
- package/dist/tips.js +168 -0
- package/dist/tips.js.map +1 -0
- package/dist/tools/builtins/image-generation-tool.d.ts +96 -0
- package/dist/tools/builtins/image-generation-tool.js +471 -0
- package/dist/tools/builtins/image-generation-tool.js.map +1 -0
- package/dist/tools/builtins/search-providers.d.ts +15 -1
- package/dist/tools/builtins/search-providers.js +195 -1
- package/dist/tools/builtins/search-providers.js.map +1 -1
- package/dist/tools/builtins/search-tool.js +2 -2
- package/dist/tools/builtins/search-tool.js.map +1 -1
- package/dist/tools/registry.d.ts +1 -0
- package/dist/tools/registry.js +11 -0
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/run-tool-use.js +1 -1
- package/dist/tools/run-tool-use.js.map +1 -1
- package/dist/tools/smoke-tool-system.js +43 -9
- package/dist/tools/smoke-tool-system.js.map +1 -1
- package/dist/tools/tool.d.ts +9 -1
- package/dist/tools/tool.js.map +1 -1
- package/dist/types/messages.d.ts +5 -0
- package/dist/types/messages.js.map +1 -1
- package/dist/ui/display-message.d.ts +103 -0
- package/dist/ui/display-message.js +115 -0
- package/dist/ui/display-message.js.map +1 -0
- package/dist/web/html.d.ts +1 -0
- package/dist/web/html.js +862 -0
- package/dist/web/html.js.map +1 -0
- package/dist/web/index.d.ts +241 -0
- package/dist/web/index.js +1873 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +1 -1
package/dist/web/html.js
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
export const WEB_HTML = String.raw `<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<title>neo web</title>
|
|
7
|
+
<link rel="stylesheet" href="/vendor/highlight-theme.css" />
|
|
8
|
+
<script defer src="/vendor/highlight.min.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
color-scheme: dark;
|
|
12
|
+
--bg: #07080b;
|
|
13
|
+
--panel: #0b0d12;
|
|
14
|
+
--text: #e5e7eb;
|
|
15
|
+
--muted: #858b98;
|
|
16
|
+
--cyan: #22d3ee;
|
|
17
|
+
--green: #22c55e;
|
|
18
|
+
--purple: #a855f7;
|
|
19
|
+
--gold: #d4b04c;
|
|
20
|
+
--red: #ef4444;
|
|
21
|
+
--yellow: #eab308;
|
|
22
|
+
--line: #161a23;
|
|
23
|
+
--page-max-width: 1120px;
|
|
24
|
+
--page-gutter: max(18px, calc((100vw - var(--page-max-width)) / 2));
|
|
25
|
+
--topbar-gutter: max(14px, calc((100vw - var(--page-max-width)) / 2));
|
|
26
|
+
}
|
|
27
|
+
* { box-sizing: border-box; }
|
|
28
|
+
html, body { height: 100%; margin: 0; }
|
|
29
|
+
body { background: radial-gradient(circle at top, #101522 0, var(--bg) 42rem); color: var(--text); font: 14px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
|
|
30
|
+
#app { height: 100%; display: flex; flex-direction: column; }
|
|
31
|
+
.topbar { height: 34px; display: flex; align-items: center; gap: 12px; padding: 0 var(--topbar-gutter); border-bottom: 1px solid var(--line); color: var(--muted); background: rgba(7, 8, 11, .75); backdrop-filter: blur(12px); }
|
|
32
|
+
.brand { color: var(--cyan); font-weight: 700; letter-spacing: .08em; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 0; background: transparent; padding: 0; font: inherit; cursor: pointer; text-align: left; }
|
|
33
|
+
.brand:hover, .brand:focus-visible { color: #67e8f9; text-decoration: underline; text-underline-offset: 3px; outline: none; }
|
|
34
|
+
.brand.session-title { letter-spacing: 0; }
|
|
35
|
+
#connection { flex: 0 0 auto; color: var(--yellow); }
|
|
36
|
+
#transcriptWrap { position: relative; flex: 1; min-height: 0; }
|
|
37
|
+
#transcript { height: 100%; overflow: auto; padding: 22px var(--page-gutter) 10px; scroll-behavior: smooth; }
|
|
38
|
+
.scroll-bottom-zone { position: absolute; left: 0; right: 0; bottom: 0; height: 22px; padding: 0 var(--page-gutter); display: flex; align-items: flex-end; opacity: 0; pointer-events: none; transition: opacity .14s ease; z-index: 2; }
|
|
39
|
+
.scroll-bottom-zone.available { opacity: 1; pointer-events: auto; }
|
|
40
|
+
#scrollBottom { width: 100%; height: 12px; border: 1px solid rgba(34, 211, 238, .42); border-radius: 999px 999px 0 0; background: linear-gradient(90deg, rgba(34, 211, 238, .06), rgba(34, 211, 238, .22), rgba(34, 211, 238, .06)); color: var(--cyan); font: inherit; font-size: 10px; line-height: 10px; letter-spacing: .12em; text-transform: uppercase; cursor: pointer; box-shadow: 0 0 18px rgba(34, 211, 238, .2), inset 0 1px 0 rgba(255,255,255,.08); text-shadow: 0 0 10px currentColor; }
|
|
41
|
+
#scrollBottom:hover, #scrollBottom:focus-visible { border-color: rgba(34, 211, 238, .82); box-shadow: 0 0 22px rgba(34, 211, 238, .42), inset 0 1px 0 rgba(255,255,255,.18); outline: none; }
|
|
42
|
+
.block { display: flex; gap: 8px; margin-top: 16px; align-items: flex-start; }
|
|
43
|
+
.block:first-child { margin-top: 0; }
|
|
44
|
+
.marker { width: 18px; flex: 0 0 18px; user-select: none; line-height: 1.45; }
|
|
45
|
+
.marker.circle { position: relative; overflow: hidden; text-indent: -999px; }
|
|
46
|
+
.marker.circle::before { content: ""; position: absolute; left: 0; top: 5px; width: 9px; height: 9px; border-radius: 50%; background: currentColor; }
|
|
47
|
+
.marker.diamond { font-size: 1em; }
|
|
48
|
+
.content { position: relative; min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
|
|
49
|
+
.content.plain { white-space: pre-wrap; }
|
|
50
|
+
.content.summary { color: #d7dce5; }
|
|
51
|
+
.kind-tool.collapsible .content { padding-right: 78px; }
|
|
52
|
+
.tool-body { position: relative; }
|
|
53
|
+
.kind-tool.collapsed .tool-body { max-height: calc(1.45em * 6); overflow: hidden; opacity: .72; mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,.84) 42%, rgba(0,0,0,.42) 76%, rgba(0,0,0,0) 100%); -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,.84) 42%, rgba(0,0,0,.42) 76%, rgba(0,0,0,0) 100%); }
|
|
54
|
+
.message-image { display: block; margin-top: 8px; max-width: min(100%, 760px); }
|
|
55
|
+
.message-image img { display: block; max-width: 100%; max-height: 70vh; border: 1px solid #202635; border-radius: 10px; background: #0c1018; box-shadow: 0 10px 30px rgba(0,0,0,.28); object-fit: contain; }
|
|
56
|
+
.message-image figcaption { margin-top: 5px; color: var(--muted); font-size: 12px; line-height: 1.35; font-weight: 400; }
|
|
57
|
+
.tool-toggle { position: absolute; top: 0; right: 0; opacity: 0; pointer-events: none; border: 1px solid #263043; border-radius: 999px; padding: 1px 8px; background: rgba(15, 23, 42, .92); color: var(--muted); font: inherit; font-size: 11px; line-height: 17px; cursor: pointer; transition: opacity .12s ease, color .12s ease, border-color .12s ease; }
|
|
58
|
+
.kind-tool.collapsible:hover .tool-toggle, .kind-tool.collapsible:focus-within .tool-toggle { opacity: 1; pointer-events: auto; }
|
|
59
|
+
.tool-toggle:hover { color: var(--cyan); border-color: #31556b; }
|
|
60
|
+
.markdown { color: var(--text); }
|
|
61
|
+
.markdown > :first-child { margin-top: 0; }
|
|
62
|
+
.markdown > :last-child { margin-bottom: 0; }
|
|
63
|
+
.markdown p { margin: 0 0 .72em; }
|
|
64
|
+
.markdown h1, .markdown h2, .markdown h3, .markdown h4 { margin: 1em 0 .45em; line-height: 1.25; color: #f3f4f6; font-weight: 700; }
|
|
65
|
+
.markdown h1 { font-size: 1.34em; padding-bottom: .22em; border-bottom: 1px solid #222837; }
|
|
66
|
+
.markdown h2 { font-size: 1.18em; }
|
|
67
|
+
.markdown h3 { font-size: 1.06em; }
|
|
68
|
+
.markdown ul, .markdown ol { margin: .35em 0 .78em; padding-left: 2.1em; }
|
|
69
|
+
.markdown li { margin: .18em 0; }
|
|
70
|
+
.markdown li > p { margin: .25em 0; }
|
|
71
|
+
.markdown blockquote { margin: .75em 0; padding: .2em 0 .2em 1em; border-left: 3px solid #334155; color: #bac2cf; background: rgba(148, 163, 184, .05); }
|
|
72
|
+
.markdown pre { position: relative; margin: .85em 0; padding: 12px 14px; overflow: auto; border: 1px solid #202635; border-radius: 8px; background: #0c1018; color: #d8dee9; white-space: pre; box-shadow: inset 0 1px 0 rgba(255,255,255,.025); }
|
|
73
|
+
.markdown pre[data-lang]::before { content: attr(data-lang); position: sticky; left: 100%; float: right; margin: -5px -7px 4px 12px; padding: 1px 6px; border: 1px solid #263043; border-radius: 999px; background: rgba(15, 23, 42, .92); color: #94a3b8; font-size: 11px; line-height: 16px; text-transform: lowercase; }
|
|
74
|
+
.markdown code { padding: .12em .34em; border: 1px solid #222838; border-radius: 5px; background: #0c1018; color: #facc15; font: .94em ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
|
|
75
|
+
.markdown pre code, .markdown pre code.hljs { display: block; padding: 0; border: 0; border-radius: 0; background: transparent; color: inherit; font-size: 1em; overflow: visible; }
|
|
76
|
+
.markdown .hljs { background: transparent; color: inherit; }
|
|
77
|
+
.markdown table { display: block; width: max-content; max-width: 100%; overflow: auto; margin: .85em 0; border-collapse: collapse; }
|
|
78
|
+
.markdown th, .markdown td { padding: 6px 10px; border: 1px solid #263043; }
|
|
79
|
+
.markdown th { background: #111827; color: #f3f4f6; font-weight: 700; }
|
|
80
|
+
.markdown tr:nth-child(2n) td { background: rgba(148, 163, 184, .045); }
|
|
81
|
+
.markdown hr { border: 0; border-top: 1px solid #222837; margin: 1em 0; }
|
|
82
|
+
.markdown a { color: var(--cyan); text-decoration: none; }
|
|
83
|
+
.markdown a:hover { text-decoration: underline; }
|
|
84
|
+
.markdown strong { color: #f8fafc; }
|
|
85
|
+
.markdown del { color: var(--muted); }
|
|
86
|
+
.markdown input[type="checkbox"] { vertical-align: -2px; margin-right: .4em; accent-color: var(--cyan); }
|
|
87
|
+
.title { color: var(--muted); font-weight: 700; margin-bottom: 2px; }
|
|
88
|
+
.body-title { color: var(--text); font-weight: 700; margin-bottom: .35em; }
|
|
89
|
+
.title.success::after { content: " ✓"; color: var(--green); }
|
|
90
|
+
.title.failure::after { content: " ✕"; color: var(--red); }
|
|
91
|
+
.kind-user .marker, .kind-user .content { color: var(--cyan); }
|
|
92
|
+
.kind-assistant .marker { color: var(--green); }
|
|
93
|
+
.kind-thinking .marker, .kind-thinking .title, .kind-thinking .content { color: var(--purple); }
|
|
94
|
+
.kind-tool .marker, .kind-tool .title { color: var(--gold); }
|
|
95
|
+
.kind-error .marker, .kind-error .title, .kind-error .content { color: var(--red); }
|
|
96
|
+
.kind-system .marker { color: #fff; }
|
|
97
|
+
.kind-meta .marker, .kind-meta .content { color: var(--muted); }
|
|
98
|
+
.live .marker { animation: pulse 900ms ease-in-out infinite; }
|
|
99
|
+
@keyframes pulse { 50% { opacity: .35; } }
|
|
100
|
+
.ansi { color: #d1d5db; }
|
|
101
|
+
.diff { color: #d1d5db; }
|
|
102
|
+
.diff-line { display: block; }
|
|
103
|
+
.diff-add { color: var(--green); }
|
|
104
|
+
.diff-del { color: var(--red); }
|
|
105
|
+
.diff-hunk { color: var(--cyan); }
|
|
106
|
+
.diff-meta { color: var(--muted); }
|
|
107
|
+
#status { flex: 0 0 auto; min-height: 28px; padding: 4px var(--page-gutter); color: var(--muted); border-top: 1px solid var(--line); display: flex; flex-direction: column; align-items: stretch; gap: 2px; overflow: hidden; white-space: nowrap; }
|
|
108
|
+
.status-main, .status-bg-row { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
109
|
+
.status-bg-row { color: var(--yellow); font-size: 12px; }
|
|
110
|
+
.phase { font-weight: 700; color: var(--green); }
|
|
111
|
+
.phase.active { color: var(--cyan); text-shadow: 0 0 12px currentColor; animation: shimmer 1.35s linear infinite; }
|
|
112
|
+
.phase.thinking { color: var(--purple); }
|
|
113
|
+
.phase.tools { color: var(--gold); }
|
|
114
|
+
.phase.stopped { color: var(--yellow); }
|
|
115
|
+
.sep { color: var(--muted); padding: 0 7px; }
|
|
116
|
+
.token-hot { font-weight: 700; }
|
|
117
|
+
.token-input-hot { color: var(--green); }
|
|
118
|
+
.token-output-hot { color: var(--cyan); }
|
|
119
|
+
.token-error-hot { color: var(--red); }
|
|
120
|
+
@keyframes shimmer { 0%, 100% { filter: brightness(.9); } 45% { filter: brightness(1.9); } }
|
|
121
|
+
#queued { display: none; padding: 0 var(--page-gutter) 4px; color: var(--yellow); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
122
|
+
#panel { display: none; flex: 0 0 auto; padding: 12px var(--page-gutter); border-top: 1px solid var(--line); background: rgba(7, 8, 11, .97); color: var(--muted); max-height: min(58vh, 560px); overflow: auto; }
|
|
123
|
+
#app.sessions-page #transcriptWrap, #app.sessions-page #status, #app.sessions-page #queued, #app.sessions-page #composerWrap { display: none; }
|
|
124
|
+
#app.sessions-page #panel { flex: 1 1 auto; max-height: none; border-top: 0; padding-top: 18px; }
|
|
125
|
+
#app.sessions-page .topbar { display: none; }
|
|
126
|
+
#panel.open { display: block; }
|
|
127
|
+
.panel-title { color: var(--cyan); font-weight: 700; margin-bottom: 6px; }
|
|
128
|
+
.panel-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 10px; }
|
|
129
|
+
.panel-subtitle { color: var(--muted); font-size: 12px; margin-top: 2px; }
|
|
130
|
+
.session-list { display: grid; gap: 8px; }
|
|
131
|
+
.session-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; padding: 10px 12px; border: 1px solid #1f2937; border-radius: 12px; background: linear-gradient(180deg, rgba(15, 23, 42, .72), rgba(11, 13, 18, .82)); color: var(--text); cursor: pointer; }
|
|
132
|
+
.session-card.selected { border-color: rgba(34, 211, 238, .78); box-shadow: 0 0 0 1px rgba(34, 211, 238, .18), 0 0 22px rgba(34, 211, 238, .12); }
|
|
133
|
+
.session-card.running { border-color: rgba(34, 197, 94, .58); }
|
|
134
|
+
.session-card.current { background: linear-gradient(180deg, rgba(8, 47, 73, .56), rgba(15, 23, 42, .82)); }
|
|
135
|
+
.session-main { min-width: 0; }
|
|
136
|
+
.session-title-line { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
137
|
+
.session-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
138
|
+
.session-badges { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
|
|
139
|
+
.session-badge { border: 1px solid #263043; border-radius: 999px; padding: 1px 7px; color: var(--muted); font-size: 11px; line-height: 17px; background: rgba(15, 23, 42, .72); }
|
|
140
|
+
.session-badge.running { color: #bbf7d0; border-color: rgba(34, 197, 94, .45); background: rgba(22, 101, 52, .22); }
|
|
141
|
+
.session-badge.current { color: #a5f3fc; border-color: rgba(34, 211, 238, .45); background: rgba(8, 145, 178, .16); }
|
|
142
|
+
.session-meta { margin-top: 7px; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
|
|
143
|
+
.session-actions { display: flex; align-items: center; gap: 6px; }
|
|
144
|
+
.panel-muted { color: var(--muted); }
|
|
145
|
+
.panel-toolbar { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
|
|
146
|
+
.panel-actions button, .login-actions button, .panel-close, .panel-primary { border: 1px solid #263043; border-radius: 999px; background: rgba(15, 23, 42, .92); color: var(--muted); font: inherit; font-size: 11px; cursor: pointer; min-height: 24px; padding: 2px 9px; }
|
|
147
|
+
.panel-actions button:disabled, .login-actions button:disabled, .panel-close:disabled, .panel-primary:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; }
|
|
148
|
+
.panel-primary { width: 34px; height: 34px; min-height: 34px; padding: 0; border-radius: 50%; color: #020617; border-color: rgba(34, 211, 238, .72); background: linear-gradient(90deg, var(--cyan), #67e8f9); font-weight: 700; font-size: 22px; line-height: 30px; }
|
|
149
|
+
.panel-actions button:hover, .login-actions button:hover, .panel-close:hover, .panel-primary:hover { color: var(--cyan); border-color: #31556b; box-shadow: 0 0 14px rgba(34, 211, 238, .18); }
|
|
150
|
+
.panel-primary:hover { color: #020617; }
|
|
151
|
+
.panel-actions button.danger:hover { color: var(--red); border-color: rgba(239, 68, 68, .5); box-shadow: 0 0 14px rgba(239, 68, 68, .14); }
|
|
152
|
+
@media (max-width: 640px) {
|
|
153
|
+
:root { --page-gutter: 12px; --topbar-gutter: 12px; }
|
|
154
|
+
.topbar { height: 40px; }
|
|
155
|
+
#panel { max-height: 72vh; padding-top: 10px; }
|
|
156
|
+
.panel-header { align-items: center; }
|
|
157
|
+
.session-card { grid-template-columns: 1fr; padding: 12px; }
|
|
158
|
+
.session-actions { justify-content: stretch; }
|
|
159
|
+
.session-actions .panel-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; width: 100%; }
|
|
160
|
+
.session-actions button { margin: 0; min-height: 36px; }
|
|
161
|
+
.session-title-line { align-items: flex-start; }
|
|
162
|
+
}
|
|
163
|
+
.login-grid { display: grid; grid-template-columns: minmax(12ch, 22ch) 1fr; gap: 6px 10px; align-items: center; }
|
|
164
|
+
.login-grid label { color: var(--muted); }
|
|
165
|
+
.login-grid input, .login-grid select { min-width: 0; border: 1px solid #263043; border-radius: 6px; padding: 4px 6px; background: #0b1020; color: var(--text); font: inherit; }
|
|
166
|
+
.login-grid input:focus, .login-grid select:focus { outline: none; border-color: #31556b; box-shadow: 0 0 14px rgba(34, 211, 238, .16); }
|
|
167
|
+
.login-actions { margin-top: 8px; display: flex; gap: 6px; justify-content: flex-end; }
|
|
168
|
+
#composerWrap { flex: 0 0 auto; padding: 0 var(--page-gutter) 16px; background: rgba(7, 8, 11, .92); }
|
|
169
|
+
#completions { display: none; margin-left: 26px; margin-bottom: 6px; color: var(--muted); max-width: calc(var(--page-max-width) - 26px); }
|
|
170
|
+
.completion-title { color: var(--cyan); font-weight: 700; }
|
|
171
|
+
.completion-row { display: grid; grid-template-columns: 4ch minmax(10ch, 32ch) 1fr; gap: 1ch; min-height: 20px; align-items: center; }
|
|
172
|
+
.completion-row.selected .num { background: var(--cyan); color: #020617; }
|
|
173
|
+
.completion-row .name { color: var(--cyan); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
174
|
+
.completion-row.reasoning .name { color: var(--purple); }
|
|
175
|
+
.completion-row .desc { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
176
|
+
.completion-footer { color: var(--muted); }
|
|
177
|
+
#composer { display: flex; gap: 8px; align-items: flex-start; }
|
|
178
|
+
#prompt { color: var(--cyan); flex: 0 0 auto; padding-top: 7px; }
|
|
179
|
+
#input { flex: 1; min-height: 32px; max-height: 35vh; resize: none; border: 0; outline: 0; padding: 7px 0; background: transparent; color: var(--text); font: inherit; line-height: 1.45; caret-color: var(--cyan); }
|
|
180
|
+
#input.command { color: var(--cyan); }
|
|
181
|
+
#input.locked { color: var(--muted); }
|
|
182
|
+
</style>
|
|
183
|
+
</head>
|
|
184
|
+
<body>
|
|
185
|
+
<div id="app">
|
|
186
|
+
<div class="topbar"><button id="brand" class="brand" type="button" title="Open sessions">neo web</button><span id="connection" hidden>connecting…</span></div>
|
|
187
|
+
<div id="transcriptWrap"><div id="transcript"></div><div id="scrollBottomZone" class="scroll-bottom-zone"><button id="scrollBottom" type="button" aria-label="Scroll to bottom">bottom</button></div></div>
|
|
188
|
+
<div id="status"></div>
|
|
189
|
+
<div id="queued"></div>
|
|
190
|
+
<div id="panel"></div>
|
|
191
|
+
<div id="composerWrap">
|
|
192
|
+
<div id="completions"></div>
|
|
193
|
+
<div id="composer"><div id="prompt">●</div><textarea id="input" spellcheck="false" autofocus></textarea></div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<script type="module">
|
|
197
|
+
import { marked } from '/vendor/marked.esm.js';
|
|
198
|
+
marked.setOptions({ gfm: true, breaks: false, async: false });
|
|
199
|
+
const TOOL_COLLAPSED_LINES = 6;
|
|
200
|
+
const STATUS_PHASE_MIN_DISPLAY_MS = 2000;
|
|
201
|
+
const TOKEN_PULSE_MS = 900;
|
|
202
|
+
const ANIMATED_NUMBER_INTERVAL_MS = 50;
|
|
203
|
+
const ANIMATED_NUMBER_MIN_DURATION_MS = 180;
|
|
204
|
+
const ANIMATED_NUMBER_MAX_DURATION_MS = 700;
|
|
205
|
+
const ANIMATED_NUMBER_DURATION_SCALE_MS = 130;
|
|
206
|
+
const state = { lines: [], status: { phase: 'ready', streamedOutputTokens: 0 }, busy: false, queuedInput: undefined, backgroundTaskCount: 0, backgroundTasks: [], backgroundSessionRunCount: 0, runningSessionIds: [], session: undefined, catalog: { commands: [], modelIds: [], reasoning: [] }, interactive: {}, tips: [], tipIndex: 0, history: [], historyIndex: undefined, completionIndex: 0, expandedToolLines: new Set(), panel: undefined, panelSelection: 0, attachments: [], attachmentCounter: 0, view: location.pathname === '/sessions' ? 'sessions' : 'chat' };
|
|
207
|
+
const animatedNumbers = { input: { target: undefined, display: undefined, timer: undefined }, output: { target: undefined, display: undefined, timer: undefined } };
|
|
208
|
+
const renderedLineKeys = new Map();
|
|
209
|
+
const statusNodes = {};
|
|
210
|
+
const phaseDisplay = { value: state.status.phase, displayedAt: Date.now(), pending: undefined, timer: undefined };
|
|
211
|
+
let renderPending = false;
|
|
212
|
+
const transcript = document.getElementById('transcript');
|
|
213
|
+
const scrollBottomZone = document.getElementById('scrollBottomZone');
|
|
214
|
+
const scrollBottom = document.getElementById('scrollBottom');
|
|
215
|
+
const statusEl = document.getElementById('status');
|
|
216
|
+
const queuedEl = document.getElementById('queued');
|
|
217
|
+
const panelEl = document.getElementById('panel');
|
|
218
|
+
const input = document.getElementById('input');
|
|
219
|
+
const completionsEl = document.getElementById('completions');
|
|
220
|
+
const brand = document.getElementById('brand');
|
|
221
|
+
const connection = document.getElementById('connection');
|
|
222
|
+
|
|
223
|
+
const sessionsPage = () => state.view === 'sessions';
|
|
224
|
+
const openSessionsOnLoad = state.view === 'sessions';
|
|
225
|
+
const es = new EventSource('/events');
|
|
226
|
+
es.addEventListener('open', () => {
|
|
227
|
+
connection.hidden = true;
|
|
228
|
+
connection.textContent = '';
|
|
229
|
+
});
|
|
230
|
+
es.addEventListener('error', () => {
|
|
231
|
+
connection.hidden = false;
|
|
232
|
+
connection.textContent = 'reconnecting…';
|
|
233
|
+
});
|
|
234
|
+
es.addEventListener('sync', (event) => {
|
|
235
|
+
const payload = JSON.parse(event.data);
|
|
236
|
+
state.lines = payload.lines || [];
|
|
237
|
+
state.status = payload.status || state.status;
|
|
238
|
+
state.busy = !!payload.busy;
|
|
239
|
+
state.queuedInput = payload.queuedInput;
|
|
240
|
+
state.backgroundTaskCount = payload.backgroundTaskCount || 0;
|
|
241
|
+
state.backgroundTasks = payload.backgroundTasks || [];
|
|
242
|
+
state.backgroundSessionRunCount = payload.backgroundSessionRunCount || 0;
|
|
243
|
+
state.runningSessionIds = payload.runningSessionIds || state.runningSessionIds || [];
|
|
244
|
+
state.session = payload.session;
|
|
245
|
+
if (payload.catalog) state.catalog = payload.catalog;
|
|
246
|
+
if (payload.interactive) state.interactive = payload.interactive;
|
|
247
|
+
if (payload.tips) state.tips = payload.tips;
|
|
248
|
+
if (payload.tipIndex !== undefined && state.tipIndex === 0) state.tipIndex = payload.tipIndex;
|
|
249
|
+
updateInputPlaceholder();
|
|
250
|
+
scheduleRender();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
function scheduleRender() {
|
|
254
|
+
if (renderPending) return;
|
|
255
|
+
renderPending = true;
|
|
256
|
+
requestAnimationFrame(() => { renderPending = false; render(); });
|
|
257
|
+
}
|
|
258
|
+
function render() { renderTranscript(); renderStatus(); renderTitle(); renderQueued(); renderPanel(); renderCompletions(); updateInputPlaceholder(); updateScrollBottomAffordance(); input.classList.toggle('locked', state.busy && state.queuedInput !== undefined); }
|
|
259
|
+
function renderTranscript() {
|
|
260
|
+
const atBottom = isTranscriptAtBottom();
|
|
261
|
+
const seen = new Set();
|
|
262
|
+
let cursor = transcript.firstElementChild;
|
|
263
|
+
for (const line of state.lines) {
|
|
264
|
+
const id = String(line.id);
|
|
265
|
+
seen.add(id);
|
|
266
|
+
let element = transcript.querySelector('[data-line-id="' + cssEscape(id) + '"]');
|
|
267
|
+
const key = lineRenderKey(line);
|
|
268
|
+
if (!element) {
|
|
269
|
+
element = document.createElement('div');
|
|
270
|
+
element.setAttribute('data-line-id', id);
|
|
271
|
+
updateLineElement(element, line);
|
|
272
|
+
renderedLineKeys.set(id, key);
|
|
273
|
+
} else if (renderedLineKeys.get(id) !== key) {
|
|
274
|
+
updateLineElement(element, line);
|
|
275
|
+
renderedLineKeys.set(id, key);
|
|
276
|
+
}
|
|
277
|
+
if (element !== cursor) transcript.insertBefore(element, cursor);
|
|
278
|
+
cursor = element.nextElementSibling;
|
|
279
|
+
}
|
|
280
|
+
for (const child of Array.from(transcript.children)) {
|
|
281
|
+
const id = child.getAttribute('data-line-id');
|
|
282
|
+
if (!seen.has(id)) { renderedLineKeys.delete(id); child.remove(); }
|
|
283
|
+
}
|
|
284
|
+
if (atBottom) transcript.scrollTop = transcript.scrollHeight;
|
|
285
|
+
}
|
|
286
|
+
function isTranscriptAtBottom() {
|
|
287
|
+
return transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
|
288
|
+
}
|
|
289
|
+
function updateScrollBottomAffordance() {
|
|
290
|
+
scrollBottomZone.classList.toggle('available', !isTranscriptAtBottom());
|
|
291
|
+
}
|
|
292
|
+
function updateLineElement(element, line) {
|
|
293
|
+
const kind = line.kind || 'system';
|
|
294
|
+
const marker = markerForLine(line, kind);
|
|
295
|
+
const markerCls = marker === '●' ? 'circle' : 'diamond';
|
|
296
|
+
const expanded = state.expandedToolLines.has(line.id);
|
|
297
|
+
const collapsible = kind === 'tool' && line.collapsible !== false && hasMoreThanLines(line.text || '', TOOL_COLLAPSED_LINES);
|
|
298
|
+
const collapsed = collapsible && !expanded;
|
|
299
|
+
const title = line.title ? '<div class="title ' + (line.titleStatus || '') + '">' + esc(line.title) + '</div>' : '';
|
|
300
|
+
const bodyTitle = line.bodyTitle ? '<div class="body-title">' + esc(line.bodyTitle) + '</div>' : '';
|
|
301
|
+
const markdown = shouldRenderMarkdown(line);
|
|
302
|
+
const cls = ['block', 'kind-' + kind, line.live ? 'live' : '', line.previewStyle === 'summary' ? 'summary-block' : '', collapsible ? 'collapsible' : '', collapsed ? 'collapsed' : '', expanded ? 'expanded' : ''].filter(Boolean).join(' ');
|
|
303
|
+
const contentCls = ['content', markdown ? 'markdown' : 'plain', line.previewStyle === 'summary' ? 'summary' : ''].filter(Boolean).join(' ');
|
|
304
|
+
const body = '<div class="tool-body">' + bodyTitle + renderText(line.text || '', line.format, markdown) + renderLineImage(line.image) + '</div>';
|
|
305
|
+
const toggle = collapsible ? '<button class="tool-toggle" type="button" data-line-id="' + String(line.id) + '" aria-expanded="' + (expanded ? 'true' : 'false') + '">' + (expanded ? 'collapse' : 'expand') + '</button>' : '';
|
|
306
|
+
element.className = cls;
|
|
307
|
+
element.innerHTML = '<div class="marker ' + markerCls + '">' + marker + '</div><div class="' + contentCls + '">' + title + body + toggle + '</div>';
|
|
308
|
+
}
|
|
309
|
+
function lineRenderKey(line) {
|
|
310
|
+
const kind = line.kind || 'system';
|
|
311
|
+
const expanded = state.expandedToolLines.has(line.id);
|
|
312
|
+
const collapsible = kind === 'tool' && line.collapsible !== false && hasMoreThanLines(line.text || '', TOOL_COLLAPSED_LINES);
|
|
313
|
+
const image = line.image ? [line.image.src || '', line.image.label || '', line.image.mimeType || ''].join('\u001e') : '';
|
|
314
|
+
return [kind, line.text || '', line.title || '', line.bodyTitle || '', line.titleStatus || '', line.format || '', line.previewStyle || '', line.summaryMaxLines || '', line.live ? '1' : '0', line.pendingReplacement ? '1' : '0', collapsible ? '1' : '0', expanded ? '1' : '0', image].join('\u001f');
|
|
315
|
+
}
|
|
316
|
+
function markerForLine(line, kind) {
|
|
317
|
+
if (kind === 'tool') return line.live || line.pendingReplacement ? '◇' : '◆';
|
|
318
|
+
if (kind === 'thinking') return '◆';
|
|
319
|
+
return '●';
|
|
320
|
+
}
|
|
321
|
+
function shouldRenderMarkdown(line) {
|
|
322
|
+
if (line.format === 'ansi' || line.format === 'plain' || line.format === 'diff') return false;
|
|
323
|
+
return line.kind === 'assistant' || line.kind === 'thinking' || line.kind === 'system' || line.kind === 'tool';
|
|
324
|
+
}
|
|
325
|
+
function hasMoreThanLines(text, maxLines) {
|
|
326
|
+
if (!text) return false;
|
|
327
|
+
let lines = 1;
|
|
328
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
329
|
+
if (text.charCodeAt(i) === 10 && ++lines > maxLines) return true;
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
function renderText(text, format, markdown) {
|
|
334
|
+
if (format === 'ansi') return '<span class="ansi">' + esc(stripAnsi(text)) + '</span>';
|
|
335
|
+
if (format === 'diff') return renderDiffText(text);
|
|
336
|
+
if (!markdown) return linkify(esc(text));
|
|
337
|
+
return sanitizeMarkdownHtml(marked.parse(text || ''));
|
|
338
|
+
}
|
|
339
|
+
function renderLineImage(image) {
|
|
340
|
+
if (!image || !safeImageSrc(image.src)) return '';
|
|
341
|
+
const label = image.label || 'generated image';
|
|
342
|
+
return '<figure class="message-image"><img src="' + esc(image.src) + '" alt="' + esc(label) + '" loading="lazy" decoding="async" /><figcaption>' + esc(label) + '</figcaption></figure>';
|
|
343
|
+
}
|
|
344
|
+
function renderDiffText(text) {
|
|
345
|
+
const lines = String(text || '').split('\n');
|
|
346
|
+
return '<span class="diff">' + lines.map((line) => {
|
|
347
|
+
const cls = diffLineClass(line);
|
|
348
|
+
return '<span class="diff-line ' + cls + '">' + esc(line) + '</span>';
|
|
349
|
+
}).join('') + '</span>';
|
|
350
|
+
}
|
|
351
|
+
function diffLineClass(line) {
|
|
352
|
+
const pipeIndex = line.indexOf('│ ');
|
|
353
|
+
const diffMarker = pipeIndex >= 0 ? line.slice(pipeIndex + 2, pipeIndex + 3) : line.slice(0, 1);
|
|
354
|
+
if (line.startsWith('@@')) return 'diff-hunk';
|
|
355
|
+
if (line.startsWith('--- ') || line.startsWith('+++ ') || line.startsWith('create ') || line.startsWith('edit ') || line.startsWith('write ') || line.startsWith('failed ') || line === 'no changes') return 'diff-meta';
|
|
356
|
+
if (diffMarker === '+') return 'diff-add';
|
|
357
|
+
if (diffMarker === '-') return 'diff-del';
|
|
358
|
+
return 'diff-meta';
|
|
359
|
+
}
|
|
360
|
+
function renderStatus() {
|
|
361
|
+
ensureStatusNodes();
|
|
362
|
+
const s = state.status || {};
|
|
363
|
+
const displayPhase = minimumDisplayPhase(s.phase || 'ready');
|
|
364
|
+
const phase = phaseLabel(displayPhase);
|
|
365
|
+
const ctx = contextParts(s.metrics);
|
|
366
|
+
const inputTokens = compactNumber(animatedNumber('input', (s.usage && s.usage.inputTokens) ?? (s.metrics && s.metrics.estimatedInputTokens)));
|
|
367
|
+
const outputTokens = compactNumber(animatedNumber('output', (s.usage && s.usage.outputTokens) ?? s.streamedOutputTokens));
|
|
368
|
+
const model = truncateMiddle((s.metrics && s.metrics.model) || 'model?', window.innerWidth > 900 ? 26 : 14);
|
|
369
|
+
const phaseActive = isActivePhase(displayPhase);
|
|
370
|
+
const phaseClass = ['phase', phaseActive ? 'active' : '', displayPhase === 'thinking' ? 'thinking' : '', displayPhase === 'running_tools' ? 'tools' : '', displayPhase === 'stopped' ? 'stopped' : ''].filter(Boolean).join(' ');
|
|
371
|
+
setText(statusNodes.phase, phase);
|
|
372
|
+
if (statusNodes.phase.className !== phaseClass) statusNodes.phase.className = phaseClass;
|
|
373
|
+
setText(statusNodes.model, model);
|
|
374
|
+
setText(statusNodes.ctxPercent, ctx.percent);
|
|
375
|
+
const ctxColor = contextColor(s.metrics);
|
|
376
|
+
if (statusNodes.ctxPercent.style.color !== ctxColor) statusNodes.ctxPercent.style.color = ctxColor;
|
|
377
|
+
const now = Date.now();
|
|
378
|
+
const retryPending = retryCooldownActive(s, now);
|
|
379
|
+
const inputArrowClass = retryPending ? 'token-hot token-error-hot' : tokenArrowHotClass(s.inputTokenUpdatedAt, now, 'token-input-hot');
|
|
380
|
+
if (statusNodes.inputArrow.className !== inputArrowClass) statusNodes.inputArrow.className = inputArrowClass;
|
|
381
|
+
setText(statusNodes.inputTokens, inputTokens);
|
|
382
|
+
const outputArrowClass = modelOutputPending(s, now) ? '' : tokenArrowHotClass(s.outputTokenUpdatedAt, now, 'token-output-hot');
|
|
383
|
+
if (statusNodes.outputArrow.className !== outputArrowClass) statusNodes.outputArrow.className = outputArrowClass;
|
|
384
|
+
setText(statusNodes.outputTokens, outputTokens);
|
|
385
|
+
renderBackgroundTasks();
|
|
386
|
+
}
|
|
387
|
+
function renderBackgroundTasks() {
|
|
388
|
+
const tasks = state.backgroundTasks || [];
|
|
389
|
+
const rows = statusNodes.backgroundRows;
|
|
390
|
+
if (!rows) return;
|
|
391
|
+
rows.innerHTML = '';
|
|
392
|
+
if (!tasks.length) { rows.style.display = 'none'; return; }
|
|
393
|
+
rows.style.display = '';
|
|
394
|
+
const summary = document.createElement('div');
|
|
395
|
+
summary.className = 'status-bg-row';
|
|
396
|
+
summary.textContent = '◇ background tools: ' + tasks.length + ' task' + (tasks.length === 1 ? '' : 's');
|
|
397
|
+
rows.appendChild(summary);
|
|
398
|
+
for (const task of tasks.slice(0, 2)) {
|
|
399
|
+
const row = document.createElement('div');
|
|
400
|
+
row.className = 'status-bg-row';
|
|
401
|
+
row.textContent = ' ' + task.type + ':' + truncateMiddle(task.description || task.agentId || task.taskId, Math.max(12, Math.floor(window.innerWidth / 18))) + ' · ' + task.status + ' · ' + formatElapsed(Date.now() - Date.parse(task.createdAt || new Date().toISOString()));
|
|
402
|
+
rows.appendChild(row);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function ensureStatusNodes() {
|
|
406
|
+
if (statusNodes.phase) return;
|
|
407
|
+
statusEl.innerHTML = '<div class="status-main"><span data-part="phase"></span><span class="sep">·</span><span data-part="model"></span><span class="sep">·</span><span data-part="ctxPercent"></span><span class="sep">·</span><span data-part="inputArrow">↑</span> <span data-part="inputTokens"></span><span class="sep">·</span><span data-part="outputArrow">↓</span> <span data-part="outputTokens"></span></div><div data-part="backgroundRows"></div>';
|
|
408
|
+
for (const node of statusEl.querySelectorAll('[data-part]')) statusNodes[node.getAttribute('data-part')] = node;
|
|
409
|
+
}
|
|
410
|
+
function minimumDisplayPhase(target) {
|
|
411
|
+
if (phaseDisplay.timer) {
|
|
412
|
+
clearTimeout(phaseDisplay.timer);
|
|
413
|
+
phaseDisplay.timer = undefined;
|
|
414
|
+
}
|
|
415
|
+
if (Object.is(target, phaseDisplay.value)) {
|
|
416
|
+
phaseDisplay.pending = undefined;
|
|
417
|
+
return phaseDisplay.value;
|
|
418
|
+
}
|
|
419
|
+
const applyPending = () => {
|
|
420
|
+
const next = phaseDisplay.pending;
|
|
421
|
+
if (next === undefined || Object.is(next, phaseDisplay.value)) {
|
|
422
|
+
phaseDisplay.pending = undefined;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
phaseDisplay.value = next;
|
|
426
|
+
phaseDisplay.displayedAt = Date.now();
|
|
427
|
+
phaseDisplay.pending = undefined;
|
|
428
|
+
phaseDisplay.timer = undefined;
|
|
429
|
+
scheduleRender();
|
|
430
|
+
};
|
|
431
|
+
phaseDisplay.pending = target;
|
|
432
|
+
const remainingMs = STATUS_PHASE_MIN_DISPLAY_MS - (Date.now() - phaseDisplay.displayedAt);
|
|
433
|
+
if (remainingMs <= 0) applyPending();
|
|
434
|
+
else phaseDisplay.timer = setTimeout(applyPending, remainingMs);
|
|
435
|
+
return phaseDisplay.value;
|
|
436
|
+
}
|
|
437
|
+
function setText(node, text) {
|
|
438
|
+
text = String(text);
|
|
439
|
+
if (node.textContent !== text) node.textContent = text;
|
|
440
|
+
}
|
|
441
|
+
function renderTitle() {
|
|
442
|
+
const title = sessionDisplayTitle(state.session);
|
|
443
|
+
const prefix = isActivePhase((state.status || {}).phase) || state.backgroundTaskCount > 0 ? '● ' : '✓ ';
|
|
444
|
+
if (title) {
|
|
445
|
+
const value = prefix + title;
|
|
446
|
+
setText(brand, value);
|
|
447
|
+
brand.classList.add('session-title');
|
|
448
|
+
if (document.title !== value) document.title = value;
|
|
449
|
+
} else {
|
|
450
|
+
setText(brand, 'neo web');
|
|
451
|
+
brand.classList.remove('session-title');
|
|
452
|
+
if (document.title !== 'neo web') document.title = 'neo web';
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function sessionDisplayTitle(session) {
|
|
456
|
+
const title = session && typeof session.title === 'string' ? session.title.trim() : '';
|
|
457
|
+
return title && title !== 'neo' ? title : '';
|
|
458
|
+
}
|
|
459
|
+
function tokenArrowHotClass(updatedAt, now, hotClass) {
|
|
460
|
+
return updatedAt !== undefined && now - updatedAt <= TOKEN_PULSE_MS ? 'token-hot ' + hotClass : '';
|
|
461
|
+
}
|
|
462
|
+
function retryCooldownActive(status, now) {
|
|
463
|
+
return status && status.retryCooldownUntil !== undefined && now < status.retryCooldownUntil;
|
|
464
|
+
}
|
|
465
|
+
function modelOutputPending(status, now) {
|
|
466
|
+
if (retryCooldownActive(status, now)) return true;
|
|
467
|
+
if (!status || status.phase !== 'calling_model') return false;
|
|
468
|
+
return tokenArrowHotClass(status.outputTokenUpdatedAt, now, 'token-output-hot') === '';
|
|
469
|
+
}
|
|
470
|
+
function animatedNumber(key, target) {
|
|
471
|
+
const item = animatedNumbers[key];
|
|
472
|
+
if (target === undefined || target === null || !Number.isFinite(Number(target))) {
|
|
473
|
+
if (item.timer) clearInterval(item.timer);
|
|
474
|
+
item.timer = undefined;
|
|
475
|
+
item.target = undefined;
|
|
476
|
+
item.display = undefined;
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
target = Number(target);
|
|
480
|
+
if (item.display === undefined || item.target === undefined) {
|
|
481
|
+
item.target = target;
|
|
482
|
+
item.display = target;
|
|
483
|
+
return item.display;
|
|
484
|
+
}
|
|
485
|
+
if (Object.is(item.target, target)) return item.display;
|
|
486
|
+
if (item.timer) clearInterval(item.timer);
|
|
487
|
+
const from = Number(item.display);
|
|
488
|
+
const delta = target - from;
|
|
489
|
+
const startedAt = Date.now();
|
|
490
|
+
const durationMs = animatedNumberDurationMs(Math.abs(delta));
|
|
491
|
+
item.target = target;
|
|
492
|
+
item.timer = setInterval(() => {
|
|
493
|
+
const progress = Math.min(1, (Date.now() - startedAt) / durationMs);
|
|
494
|
+
const eased = easeOutCubic(progress);
|
|
495
|
+
item.display = progress >= 1 ? target : from + delta * eased;
|
|
496
|
+
if (progress >= 1) { clearInterval(item.timer); item.timer = undefined; }
|
|
497
|
+
scheduleRender();
|
|
498
|
+
}, ANIMATED_NUMBER_INTERVAL_MS);
|
|
499
|
+
return item.display;
|
|
500
|
+
}
|
|
501
|
+
function animatedNumberDurationMs(delta) {
|
|
502
|
+
if (!Number.isFinite(delta) || delta <= 0) return ANIMATED_NUMBER_MIN_DURATION_MS;
|
|
503
|
+
const scaled = ANIMATED_NUMBER_MIN_DURATION_MS + Math.log10(delta + 1) * ANIMATED_NUMBER_DURATION_SCALE_MS;
|
|
504
|
+
return Math.min(ANIMATED_NUMBER_MAX_DURATION_MS, Math.max(ANIMATED_NUMBER_MIN_DURATION_MS, scaled));
|
|
505
|
+
}
|
|
506
|
+
function easeOutCubic(progress) {
|
|
507
|
+
const clamped = Math.max(0, Math.min(1, progress));
|
|
508
|
+
return 1 - Math.pow(1 - clamped, 3);
|
|
509
|
+
}
|
|
510
|
+
function renderQueued() {
|
|
511
|
+
if (!state.queuedInput) { if (queuedEl.style.display !== 'none') queuedEl.style.display = 'none'; return; }
|
|
512
|
+
if (queuedEl.style.display !== 'block') queuedEl.style.display = 'block';
|
|
513
|
+
setText(queuedEl, 'pending next: ' + state.queuedInput.replace(/\s+/g, ' ').trim() + ' (Esc/Ctrl+C to clear)');
|
|
514
|
+
}
|
|
515
|
+
function renderPanel() {
|
|
516
|
+
document.getElementById('app').classList.toggle('sessions-page', sessionsPage());
|
|
517
|
+
if (!state.panel) { panelEl.className = ''; panelEl.innerHTML = ''; return; }
|
|
518
|
+
panelEl.className = 'open';
|
|
519
|
+
if (state.panel === 'sessions') renderSessionsPanel();
|
|
520
|
+
else if (state.panel === 'login') renderLoginPanel();
|
|
521
|
+
}
|
|
522
|
+
async function openSessionsPanel() {
|
|
523
|
+
state.view = 'sessions';
|
|
524
|
+
state.panel = 'sessions';
|
|
525
|
+
state.panelSelection = 0;
|
|
526
|
+
history.replaceState(null, '', '/sessions');
|
|
527
|
+
renderPanel();
|
|
528
|
+
panelEl.className = 'open';
|
|
529
|
+
panelEl.innerHTML = '<div class="panel-title">Sessions</div><div class="panel-muted">loading…</div>';
|
|
530
|
+
const res = await fetch('/api/sessions');
|
|
531
|
+
const body = await res.json();
|
|
532
|
+
state.sessions = body.sessions || [];
|
|
533
|
+
state.runningSessionIds = body.runningSessionIds || [];
|
|
534
|
+
renderPanel();
|
|
535
|
+
}
|
|
536
|
+
function renderSessionsPanel() {
|
|
537
|
+
const sessions = state.sessions || [];
|
|
538
|
+
const selected = Math.max(0, Math.min(state.panelSelection, sessions.length - 1));
|
|
539
|
+
state.panelSelection = selected;
|
|
540
|
+
const currentSessionId = state.session && state.session.sessionId;
|
|
541
|
+
const header = '<div class="panel-header"><div><div class="panel-title">Sessions</div><div class="panel-subtitle">Manage saved sessions.</div></div><div class="panel-toolbar"><button class="panel-primary" data-action="new-session" title="New session" aria-label="New session">+</button></div></div>';
|
|
542
|
+
const body = sessions.length ? '<div class="session-list">' + sessions.map((s, i) => renderSessionCard(s, i, selected, currentSessionId)).join('') + '</div>' : '<div class="panel-muted">No saved sessions found. Tap + to start a new session.</div>';
|
|
543
|
+
panelEl.innerHTML = header + body + '<div class="panel-muted" style="margin-top:8px">↑/↓ select · Enter enter · Delete remove</div>';
|
|
544
|
+
}
|
|
545
|
+
function renderSessionCard(s, i, selected, currentSessionId) {
|
|
546
|
+
const isCurrent = s.sessionId === currentSessionId;
|
|
547
|
+
const isRunning = (state.runningSessionIds || []).includes(s.sessionId) || (isCurrent && (state.busy || isActivePhase((state.status || {}).phase)));
|
|
548
|
+
const badges = [
|
|
549
|
+
isRunning ? '<span class="session-badge running">● running</span>' : '',
|
|
550
|
+
isCurrent ? '<span class="session-badge current">current</span>' : '',
|
|
551
|
+
'<span class="session-badge">' + esc(s.messages) + ' messages</span>',
|
|
552
|
+
].filter(Boolean).join('');
|
|
553
|
+
const classes = ['session-card', i === selected ? 'selected' : '', isCurrent ? 'current' : '', isRunning ? 'running' : ''].filter(Boolean).join(' ');
|
|
554
|
+
return '<div class="' + classes + '" data-session-index="' + i + '"><div class="session-main"><div class="session-title-line"><span class="session-name">' + esc(s.title || '(untitled)') + '</span></div><div class="session-badges">' + badges + '</div><div class="session-meta">' + esc(truncateMiddle(s.sessionId, 28)) + ' · updated ' + esc(s.updatedAt || 'unknown') + '</div></div><div class="session-actions"><span class="panel-actions"><button data-action="enter" data-session-id="' + esc(s.sessionId) + '">enter</button><button class="danger" data-action="delete" data-session-id="' + esc(s.sessionId) + '">delete</button></span></div></div>';
|
|
555
|
+
}
|
|
556
|
+
function showChatView() {
|
|
557
|
+
state.view = 'chat';
|
|
558
|
+
state.panel = undefined;
|
|
559
|
+
history.replaceState(null, '', '/');
|
|
560
|
+
renderPanel();
|
|
561
|
+
input.focus();
|
|
562
|
+
}
|
|
563
|
+
async function enterSession(sessionId) {
|
|
564
|
+
const result = await postJson('/api/sessions/resume', { sessionId });
|
|
565
|
+
if (result.ok) showChatView();
|
|
566
|
+
}
|
|
567
|
+
async function createAndEnterSession() {
|
|
568
|
+
const result = await postJson('/api/sessions/new', {});
|
|
569
|
+
if (result.ok) showChatView();
|
|
570
|
+
}
|
|
571
|
+
async function openSessionsPanelAfterDelete(sessionId) {
|
|
572
|
+
const session = (state.sessions || []).find(s => s.sessionId === sessionId);
|
|
573
|
+
const label = session ? (session.title || session.sessionId) : sessionId;
|
|
574
|
+
if (!confirm('Delete session "' + label + '"? This cannot be undone.')) return;
|
|
575
|
+
await postJson('/api/sessions/delete', { sessionId });
|
|
576
|
+
await openSessionsPanel();
|
|
577
|
+
}
|
|
578
|
+
async function openLoginPanel() {
|
|
579
|
+
state.panel = 'login';
|
|
580
|
+
panelEl.className = 'open';
|
|
581
|
+
panelEl.innerHTML = '<div class="panel-title">Provider login</div><div class="panel-muted">loading…</div>';
|
|
582
|
+
const res = await fetch('/api/login');
|
|
583
|
+
state.login = await res.json();
|
|
584
|
+
renderPanel();
|
|
585
|
+
}
|
|
586
|
+
function renderLoginPanel() {
|
|
587
|
+
const login = state.login || state.interactive.login;
|
|
588
|
+
if (!login) { panelEl.innerHTML = '<div class="panel-title">Provider login</div><div class="panel-muted">Login config unavailable.</div>'; return; }
|
|
589
|
+
const fields = login.fields || [];
|
|
590
|
+
panelEl.innerHTML = '<div class="panel-title">Provider login <span class="panel-muted">' + esc(login.envPath || '') + '</span></div><div class="login-grid"><label>Provider</label><select data-login-provider>' + (login.providers || []).map(p => '<option value="' + esc(p) + '" ' + (p === login.provider ? 'selected' : '') + '>' + esc(p) + '</option>').join('') + '</select>' + fields.map(field => '<label>' + esc(field.label) + (field.required ? ' *' : '') + '</label>' + loginFieldControl(field, login.values && login.values[field.key])).join('') + '</div><div class="login-actions"><button data-action="login-save">save</button><button data-action="panel-close">cancel</button></div><div class="panel-muted">Shared runtime fields save as MODEL_*; provider fields save as OPENAI_* / DEEPSEEK_* / KIMI_*.</div>';
|
|
591
|
+
}
|
|
592
|
+
function loginFieldControl(field, value) {
|
|
593
|
+
value = value || '';
|
|
594
|
+
if (field.options && field.options.length) return '<select data-login-field="' + esc(field.key) + '">' + field.options.map(option => '<option value="' + esc(option) + '" ' + (option === value ? 'selected' : '') + '>' + esc(option || '<default>') + '</option>').join('') + '</select>';
|
|
595
|
+
return '<input data-login-field="' + esc(field.key) + '" type="' + (field.secret ? 'password' : 'text') + '" value="' + esc(value) + '" placeholder="' + esc(field.placeholder || '') + '">';
|
|
596
|
+
}
|
|
597
|
+
function completions() {
|
|
598
|
+
const text = input.value;
|
|
599
|
+
const cursor = input.selectionStart || 0;
|
|
600
|
+
const prefix = text.slice(0, cursor);
|
|
601
|
+
const suffix = text.slice(cursor);
|
|
602
|
+
if (!prefix.startsWith('/') || /\r|\n/.test(prefix) || /\S/.test(suffix)) return [];
|
|
603
|
+
if (prefix.startsWith('/model') && (prefix.length === 6 || prefix[6] === ' ')) return modelCompletions(prefix);
|
|
604
|
+
if (prefix.length > 1 && !/^\/[\w-]*$/.test(prefix)) return [];
|
|
605
|
+
const normalized = prefix.toLowerCase();
|
|
606
|
+
return (state.catalog.commands || []).flatMap(c => [c.name].concat(c.aliases || []).map(name => ({ value: name, insertText: name, description: c.description, arguments: c.arguments, kind: 'command' }))).filter(c => c.value.toLowerCase().startsWith(normalized));
|
|
607
|
+
}
|
|
608
|
+
function modelCompletions(prefix) {
|
|
609
|
+
const hasTrailingSpace = /\s$/.test(prefix);
|
|
610
|
+
const tokens = prefix.trim().split(/\s+/).filter(Boolean);
|
|
611
|
+
const args = tokens.slice(1);
|
|
612
|
+
if (args.length >= 2 && !hasTrailingSpace) return reasoningCompletions(args[0] || '', args[1] || '');
|
|
613
|
+
if (args.length >= 2) return [];
|
|
614
|
+
if (args.length === 1 && hasTrailingSpace) return reasoningCompletions(args[0] || '', '');
|
|
615
|
+
const current = args[0] || '';
|
|
616
|
+
const models = (state.catalog.modelIds || []).filter(id => id.toLowerCase().includes(current.toLowerCase())).slice(0, 80).map(id => ({ value: id, insertText: '/model ' + id, description: 'model id', arguments: 'optional', kind: 'model' }));
|
|
617
|
+
const reasoning = reasoningCompletions('', current);
|
|
618
|
+
return models.concat(reasoning);
|
|
619
|
+
}
|
|
620
|
+
function reasoningCompletions(modelId, current) { return (state.catalog.reasoning || []).filter(x => x.startsWith((current || '').toLowerCase())).map(x => ({ value: x, insertText: modelId ? '/model ' + modelId + ' ' + x : '/model ' + x, description: x === 'default' ? 'use env/provider default' : x === 'off' ? 'send no reasoning config' : 'reasoning effort: ' + x, arguments: 'optional', kind: 'reasoning' })); }
|
|
621
|
+
function renderCompletions() {
|
|
622
|
+
const list = completions();
|
|
623
|
+
input.classList.toggle('command', input.value.startsWith('/'));
|
|
624
|
+
if (!list.length || state.busy && state.queuedInput !== undefined) { completionsEl.style.display = 'none'; return; }
|
|
625
|
+
const selected = Math.max(0, Math.min(state.completionIndex, list.length - 1));
|
|
626
|
+
state.completionIndex = selected;
|
|
627
|
+
const pageSize = 10;
|
|
628
|
+
const pageStart = Math.floor(selected / pageSize) * pageSize;
|
|
629
|
+
const visible = list.slice(pageStart, pageStart + pageSize);
|
|
630
|
+
const pageCount = Math.ceil(list.length / pageSize);
|
|
631
|
+
const title = pageCount > 1 ? 'Completions (' + list.length + ') page ' + (Math.floor(pageStart / pageSize) + 1) + '/' + pageCount : 'Completions (' + list.length + ')';
|
|
632
|
+
completionsEl.style.display = 'block';
|
|
633
|
+
completionsEl.innerHTML = '<div class="completion-title">' + esc(title) + '</div>' + visible.map((c, i) => '<div class="completion-row ' + (c.kind || '') + ' ' + (i + pageStart === selected ? 'selected' : '') + '"><span class="num">' + (i + pageStart + 1) + '.</span><span class="name">' + esc(c.value) + '</span><span class="desc">' + esc(c.description || '') + '</span></div>').join('') + '<div class="completion-footer">↑/↓ select · ←/→ page · Tab complete</div>';
|
|
634
|
+
}
|
|
635
|
+
function selectedCompletion() { const list = completions(); return list.length ? list[Math.max(0, Math.min(state.completionIndex, list.length - 1))] : undefined; }
|
|
636
|
+
function completeSelection() { const c = selectedCompletion(); if (!c) return false; const cursor = input.selectionStart || 0; input.value = c.insertText + input.value.slice(cursor); input.selectionStart = input.selectionEnd = c.insertText.length; advanceTip(); autosize(); renderCompletions(); return true; }
|
|
637
|
+
function currentTip() { const tips = state.tips || []; return tips.length ? tips[((state.tipIndex % tips.length) + tips.length) % tips.length] : undefined; }
|
|
638
|
+
function advanceTip(delta = 1) { state.tipIndex += delta; updateInputPlaceholder(); }
|
|
639
|
+
function updateInputPlaceholder() { const tip = currentTip(); input.placeholder = tip ? tip.placeholder : 'Type a message, or /help for commands'; }
|
|
640
|
+
async function postJson(url, body) { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const value = await res.json(); if (!value.ok && value.error) alert(value.error); return value; }
|
|
641
|
+
async function saveLoginPanel() {
|
|
642
|
+
const login = state.login;
|
|
643
|
+
if (!login) return;
|
|
644
|
+
const values = {};
|
|
645
|
+
for (const el of panelEl.querySelectorAll('[data-login-field]')) values[el.getAttribute('data-login-field')] = el.value;
|
|
646
|
+
const provider = panelEl.querySelector('[data-login-provider]')?.value || login.provider;
|
|
647
|
+
const result = await postJson('/api/login', { provider, values });
|
|
648
|
+
if (result.ok) { state.panel = undefined; renderPanel(); }
|
|
649
|
+
}
|
|
650
|
+
function attachmentsForText(text) { return state.attachments.filter(attachment => text.includes(attachment.label)); }
|
|
651
|
+
function insertAtCursor(value) { const start = input.selectionStart || 0, end = input.selectionEnd || start; input.value = input.value.slice(0, start) + value + input.value.slice(end); input.selectionStart = input.selectionEnd = start + value.length; advanceTip(); autosize(); renderCompletions(); }
|
|
652
|
+
async function fileToDataUrlPayload(file) {
|
|
653
|
+
const dataUrl = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '')); reader.onerror = () => reject(reader.error || new Error('read failed')); reader.readAsDataURL(file); });
|
|
654
|
+
const comma = dataUrl.indexOf(',');
|
|
655
|
+
return { mimeType: file.type || 'image/png', data: comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl };
|
|
656
|
+
}
|
|
657
|
+
async function handlePaste(e) {
|
|
658
|
+
const files = Array.from(e.clipboardData?.files || []).filter(file => file.type.startsWith('image/'));
|
|
659
|
+
if (!files.length) return;
|
|
660
|
+
e.preventDefault();
|
|
661
|
+
for (const file of files) {
|
|
662
|
+
const id = ++state.attachmentCounter;
|
|
663
|
+
const label = '[img#' + id + ']';
|
|
664
|
+
const payload = await fileToDataUrlPayload(file);
|
|
665
|
+
state.attachments.push({ kind: 'image', label, mimeType: payload.mimeType, data: payload.data });
|
|
666
|
+
insertAtCursor(label);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async function submit() {
|
|
670
|
+
const text = input.value;
|
|
671
|
+
if (text.trim() === '/sessions') { input.value = ''; autosize(); renderCompletions(); await openSessionsPanel(); return; }
|
|
672
|
+
if (text.trim() === '/login') { input.value = ''; autosize(); renderCompletions(); await openLoginPanel(); return; }
|
|
673
|
+
if (text.trim() === '/new') {
|
|
674
|
+
input.value = '';
|
|
675
|
+
state.attachments = [];
|
|
676
|
+
autosize();
|
|
677
|
+
renderCompletions();
|
|
678
|
+
const result = await postJson('/api/sessions/new', {});
|
|
679
|
+
if (!result.ok && result.error) alert(result.error);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const attachments = attachmentsForText(text);
|
|
683
|
+
if (!text.trim() && attachments.length === 0) return;
|
|
684
|
+
state.history = [text].concat(state.history.filter(x => x !== text)).slice(0, 100);
|
|
685
|
+
state.historyIndex = undefined;
|
|
686
|
+
input.value = '';
|
|
687
|
+
state.attachments = [];
|
|
688
|
+
if (state.busy) {
|
|
689
|
+
state.busy = false;
|
|
690
|
+
state.queuedInput = undefined;
|
|
691
|
+
}
|
|
692
|
+
autosize();
|
|
693
|
+
renderCompletions();
|
|
694
|
+
const res = await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, attachments }) });
|
|
695
|
+
const body = await res.json();
|
|
696
|
+
if (!body.ok && body.error) alert(body.error);
|
|
697
|
+
}
|
|
698
|
+
transcript.addEventListener('scroll', updateScrollBottomAffordance, { passive: true });
|
|
699
|
+
scrollBottom.addEventListener('click', () => { transcript.scrollTo({ top: transcript.scrollHeight, behavior: 'smooth' }); updateScrollBottomAffordance(); });
|
|
700
|
+
brand.addEventListener('click', () => { void openSessionsPanel(); });
|
|
701
|
+
panelEl.addEventListener('click', async (e) => {
|
|
702
|
+
const button = e.target.closest('button');
|
|
703
|
+
if (!button) return;
|
|
704
|
+
const action = button.getAttribute('data-action');
|
|
705
|
+
if (action === 'panel-close') { state.panel = undefined; renderPanel(); input.focus(); return; }
|
|
706
|
+
if (action === 'new-session') { await createAndEnterSession(); return; }
|
|
707
|
+
if (action === 'enter') { await enterSession(button.getAttribute('data-session-id')); return; }
|
|
708
|
+
if (action === 'delete') { await openSessionsPanelAfterDelete(button.getAttribute('data-session-id')); return; }
|
|
709
|
+
if (action === 'login-save') { await saveLoginPanel(); return; }
|
|
710
|
+
});
|
|
711
|
+
panelEl.addEventListener('change', async (e) => {
|
|
712
|
+
const provider = e.target.closest('[data-login-provider]');
|
|
713
|
+
if (!provider) return;
|
|
714
|
+
const res = await fetch('/api/login?provider=' + encodeURIComponent(provider.value));
|
|
715
|
+
state.login = await res.json();
|
|
716
|
+
renderPanel();
|
|
717
|
+
});
|
|
718
|
+
transcript.addEventListener('click', (e) => {
|
|
719
|
+
const button = e.target.closest('.tool-toggle');
|
|
720
|
+
if (!button) return;
|
|
721
|
+
const id = Number(button.getAttribute('data-line-id'));
|
|
722
|
+
if (!Number.isFinite(id)) return;
|
|
723
|
+
if (state.expandedToolLines.has(id)) state.expandedToolLines.delete(id);
|
|
724
|
+
else state.expandedToolLines.add(id);
|
|
725
|
+
const line = state.lines.find(x => x.id === id);
|
|
726
|
+
const element = transcript.querySelector('[data-line-id="' + cssEscape(String(id)) + '"]');
|
|
727
|
+
if (line && element) {
|
|
728
|
+
updateLineElement(element, line);
|
|
729
|
+
renderedLineKeys.set(String(id), lineRenderKey(line));
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
function handleSessionsKey(e) {
|
|
733
|
+
if (state.panel !== 'sessions') return false;
|
|
734
|
+
const countSessions = (state.sessions || []).length;
|
|
735
|
+
if (e.key === 'Escape') { e.preventDefault(); showChatView(); return true; }
|
|
736
|
+
if (e.key === 'ArrowUp' && countSessions) { e.preventDefault(); state.panelSelection = (state.panelSelection + countSessions - 1) % countSessions; renderPanel(); return true; }
|
|
737
|
+
if (e.key === 'ArrowDown' && countSessions) { e.preventDefault(); state.panelSelection = (state.panelSelection + 1) % countSessions; renderPanel(); return true; }
|
|
738
|
+
if (e.key === 'Enter' && countSessions) { e.preventDefault(); const s = state.sessions[state.panelSelection]; if (s) void enterSession(s.sessionId); return true; }
|
|
739
|
+
if ((e.key === 'Delete' || e.key === 'Backspace') && countSessions) { e.preventDefault(); const s = state.sessions[state.panelSelection]; if (s) openSessionsPanelAfterDelete(s.sessionId); return true; }
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
input.addEventListener('keydown', (e) => {
|
|
743
|
+
const count = completions().length;
|
|
744
|
+
if (handleSessionsKey(e)) return;
|
|
745
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); const c = selectedCompletion(); if (c && c.kind === 'command' && c.arguments !== 'none') { completeSelection(); input.value += ' '; input.selectionStart = input.selectionEnd = input.value.length; return; } submit(); return; }
|
|
746
|
+
if (e.key === 'Tab') { if (completeSelection()) e.preventDefault(); else if (!input.value) { e.preventDefault(); advanceTip(); } return; }
|
|
747
|
+
if (e.key === 'ArrowUp' && count) { e.preventDefault(); state.completionIndex = (state.completionIndex + count - 1) % count; renderCompletions(); return; }
|
|
748
|
+
if (e.key === 'ArrowDown' && count) { e.preventDefault(); state.completionIndex = (state.completionIndex + 1) % count; renderCompletions(); return; }
|
|
749
|
+
if (e.key === 'ArrowLeft' && count > 10) { e.preventDefault(); state.completionIndex = (state.completionIndex + count - 10) % count; renderCompletions(); return; }
|
|
750
|
+
if (e.key === 'ArrowRight' && count > 10) { e.preventDefault(); state.completionIndex = (state.completionIndex + 10) % count; renderCompletions(); return; }
|
|
751
|
+
if (e.key === 'ArrowUp' && !input.value && state.history.length) { e.preventDefault(); state.historyIndex = Math.min(state.history.length - 1, (state.historyIndex ?? -1) + 1); input.value = state.history[state.historyIndex] || ''; autosize(); return; }
|
|
752
|
+
if (e.key === 'ArrowUp' && !input.value) { e.preventDefault(); advanceTip(-1); return; }
|
|
753
|
+
if (e.key === 'ArrowDown' && state.historyIndex !== undefined) { e.preventDefault(); state.historyIndex -= 1; if (state.historyIndex < 0) { state.historyIndex = undefined; input.value = ''; } else input.value = state.history[state.historyIndex] || ''; autosize(); return; }
|
|
754
|
+
if (e.key === 'ArrowDown' && !input.value) { e.preventDefault(); advanceTip(); return; }
|
|
755
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'c') { if (input.value) { input.value = ''; autosize(); renderCompletions(); } else fetch('/api/interrupt', { method: 'POST' }); }
|
|
756
|
+
if (e.key === 'Escape') { state.completionIndex = 0; if (state.queuedInput) fetch('/api/interrupt', { method: 'POST' }); else renderCompletions(); }
|
|
757
|
+
});
|
|
758
|
+
document.addEventListener('keydown', (e) => {
|
|
759
|
+
if (e.target === input || e.target.closest('input, textarea, select')) return;
|
|
760
|
+
handleSessionsKey(e);
|
|
761
|
+
});
|
|
762
|
+
input.addEventListener('input', () => { state.completionIndex = 0; state.attachments = attachmentsForText(input.value); advanceTip(); autosize(); renderCompletions(); });
|
|
763
|
+
input.addEventListener('paste', (e) => { void handlePaste(e); });
|
|
764
|
+
function autosize() { input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, window.innerHeight * .35) + 'px'; updateScrollBottomAffordance(); }
|
|
765
|
+
function phaseLabel(phase) { if (phase === 'calling_model') return 'model'; if (phase === 'thinking') return 'think'; if (phase === 'running_tools') return 'tools'; if (phase === 'injecting_context') return 'context'; return phase || 'ready'; }
|
|
766
|
+
function isActivePhase(phase) { return ['running', 'preparing', 'calling_model', 'thinking', 'running_tools', 'compacting', 'injecting_context'].includes(phase); }
|
|
767
|
+
function contextParts(metrics) { if (!metrics) return { percent: '?' }; return { percent: metrics.contextUsageRatio === undefined ? '?' : (metrics.contextUsageRatio * 100).toFixed(1) + '%' }; }
|
|
768
|
+
function contextColor(metrics) { const r = metrics && metrics.contextUsageRatio; if (r === undefined) return 'var(--muted)'; if (r >= .9) return 'var(--red)'; if (r >= .75) return 'var(--yellow)'; return 'var(--muted)'; }
|
|
769
|
+
function compactNumber(value) { if (value === undefined || value === null) return '?'; const n = Math.max(0, Math.round(value)); if (n >= 1000000) return trimFixed(n / 1000000) + 'm'; if (n >= 10000) return Math.round(n / 1000) + 'k'; if (n >= 1000) return trimFixed(n / 1000) + 'k'; return String(n); }
|
|
770
|
+
function formatElapsed(ms) { const seconds = Math.max(0, Math.floor(ms / 1000)); if (seconds < 60) return seconds + 's'; const minutes = Math.floor(seconds / 60); const rem = String(seconds % 60).padStart(2, '0'); if (minutes < 60) return minutes + 'm' + rem + 's'; return Math.floor(minutes / 60) + 'h' + String(minutes % 60).padStart(2, '0') + 'm'; }
|
|
771
|
+
function trimFixed(v) { return v >= 10 ? v.toFixed(0) : v.toFixed(1).replace(/\.0$/, ''); }
|
|
772
|
+
function truncateMiddle(value, max) { value = String(value); if (value.length <= max) return value; if (max <= 3) return value.slice(0, max); const l = Math.ceil((max - 3) / 2), r = Math.floor((max - 3) / 2); return value.slice(0, l) + '...' + value.slice(value.length - r); }
|
|
773
|
+
function stripAnsi(value) { return String(value).replace(/\x1b\[[0-9;]*m/g, ''); }
|
|
774
|
+
function cssEscape(value) { return window.CSS && CSS.escape ? CSS.escape(value) : String(value).replace(/[^a-zA-Z0-9_-]/g, '\\$&'); }
|
|
775
|
+
function sanitizeMarkdownHtml(html) {
|
|
776
|
+
const template = document.createElement('template');
|
|
777
|
+
template.innerHTML = String(html);
|
|
778
|
+
const allowed = new Set(['A', 'P', 'BR', 'STRONG', 'B', 'EM', 'I', 'CODE', 'PRE', 'BLOCKQUOTE', 'UL', 'OL', 'LI', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HR', 'TABLE', 'THEAD', 'TBODY', 'TR', 'TH', 'TD', 'DEL', 'S', 'INPUT', 'TASK-LIST']);
|
|
779
|
+
const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT);
|
|
780
|
+
const nodes = [];
|
|
781
|
+
while (walker.nextNode()) nodes.push(walker.currentNode);
|
|
782
|
+
for (const node of nodes) {
|
|
783
|
+
if (!allowed.has(node.tagName)) {
|
|
784
|
+
node.replaceWith(document.createTextNode(node.textContent || ''));
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
for (const attr of Array.from(node.attributes)) {
|
|
788
|
+
const name = attr.name.toLowerCase();
|
|
789
|
+
const value = attr.value;
|
|
790
|
+
const keep = (node.tagName === 'A' && name === 'href' && safeHref(value)) ||
|
|
791
|
+
(node.tagName === 'A' && name === 'title') ||
|
|
792
|
+
(node.tagName === 'CODE' && name === 'class' && /^language-[\w-]+$/.test(value)) ||
|
|
793
|
+
(node.tagName === 'INPUT' && (name === 'type' || name === 'checked' || name === 'disabled'));
|
|
794
|
+
if (!keep) node.removeAttribute(attr.name);
|
|
795
|
+
}
|
|
796
|
+
if (node.tagName === 'A') {
|
|
797
|
+
node.setAttribute('target', '_blank');
|
|
798
|
+
node.setAttribute('rel', 'noreferrer noopener');
|
|
799
|
+
}
|
|
800
|
+
if (node.tagName === 'INPUT') {
|
|
801
|
+
if (node.getAttribute('type') !== 'checkbox') node.remove();
|
|
802
|
+
else node.setAttribute('disabled', '');
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
highlightMarkdownCodeBlocks(template.content);
|
|
806
|
+
return template.innerHTML;
|
|
807
|
+
}
|
|
808
|
+
function highlightMarkdownCodeBlocks(root) {
|
|
809
|
+
const highlighter = window.hljs;
|
|
810
|
+
for (const code of root.querySelectorAll('pre > code')) {
|
|
811
|
+
const language = normalizeCodeLanguage(code.className);
|
|
812
|
+
const pre = code.parentElement;
|
|
813
|
+
if (pre && language) pre.setAttribute('data-lang', language);
|
|
814
|
+
if (!highlighter) continue;
|
|
815
|
+
try {
|
|
816
|
+
const source = code.textContent || '';
|
|
817
|
+
const canUseLanguage = language && highlighter.getLanguage(language);
|
|
818
|
+
const result = canUseLanguage
|
|
819
|
+
? highlighter.highlight(source, { language, ignoreIllegals: true })
|
|
820
|
+
: source.length <= 20000
|
|
821
|
+
? highlighter.highlightAuto(source)
|
|
822
|
+
: undefined;
|
|
823
|
+
if (!result) continue;
|
|
824
|
+
code.innerHTML = result.value;
|
|
825
|
+
code.className = ['hljs', result.language ? 'language-' + result.language : language ? 'language-' + language : ''].filter(Boolean).join(' ');
|
|
826
|
+
if (pre && result.language && !pre.hasAttribute('data-lang')) pre.setAttribute('data-lang', result.language);
|
|
827
|
+
} catch {
|
|
828
|
+
code.textContent = code.textContent || '';
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function normalizeCodeLanguage(className) {
|
|
833
|
+
const match = /(?:^|\s)language-([\w-]+)/.exec(className || '') || /(?:^|\s)lang-([\w-]+)/.exec(className || '');
|
|
834
|
+
if (!match) return '';
|
|
835
|
+
const value = match[1].toLowerCase();
|
|
836
|
+
const aliases = { cjs: 'javascript', js: 'javascript', jsx: 'javascript', mjs: 'javascript', node: 'javascript', py: 'python', python3: 'python', sh: 'bash', shell: 'bash', ts: 'typescript', tsx: 'typescript', yml: 'yaml' };
|
|
837
|
+
return aliases[value] || value;
|
|
838
|
+
}
|
|
839
|
+
function safeHref(value) {
|
|
840
|
+
try {
|
|
841
|
+
const url = new URL(value, window.location.href);
|
|
842
|
+
return url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'mailto:';
|
|
843
|
+
} catch { return false; }
|
|
844
|
+
}
|
|
845
|
+
function safeImageSrc(value) {
|
|
846
|
+
if (typeof value !== 'string') return false;
|
|
847
|
+
if (/^data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=\s]+$/i.test(value)) return true;
|
|
848
|
+
try {
|
|
849
|
+
const url = new URL(value, window.location.href);
|
|
850
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
851
|
+
} catch { return false; }
|
|
852
|
+
}
|
|
853
|
+
function esc(value) { return String(value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
|
|
854
|
+
function linkify(value) { return value.replace(/(https?:\/\/[^\s<]+)/g, '<a style="color:var(--cyan)" target="_blank" href="$1">$1</a>'); }
|
|
855
|
+
document.getElementById('app').classList.toggle('sessions-page', sessionsPage());
|
|
856
|
+
updateInputPlaceholder();
|
|
857
|
+
autosize();
|
|
858
|
+
if (openSessionsOnLoad) void openSessionsPanel();
|
|
859
|
+
</script>
|
|
860
|
+
</body>
|
|
861
|
+
</html>`;
|
|
862
|
+
//# sourceMappingURL=html.js.map
|