koneck 2.26.0 → 2.28.0
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/doctor.d.ts +27 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +70 -9
- package/dist/doctor.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +4 -1
- package/dist/engine.js.map +1 -1
- package/dist/ink-chat.d.ts +12 -3
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +246 -30
- package/dist/ink-chat.js.map +1 -1
- package/dist/quality-gate.d.ts +5 -1
- package/dist/quality-gate.d.ts.map +1 -1
- package/dist/quality-gate.js +8 -6
- package/dist/quality-gate.js.map +1 -1
- package/dist/session.d.ts +10 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +12 -1
- package/dist/session.js.map +1 -1
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +122 -27
- package/dist/web/api.js.map +1 -1
- package/dist/web/changes.d.ts +55 -0
- package/dist/web/changes.d.ts.map +1 -0
- package/dist/web/changes.js +215 -0
- package/dist/web/changes.js.map +1 -0
- package/dist/web/files.d.ts +43 -0
- package/dist/web/files.d.ts.map +1 -0
- package/dist/web/files.js +131 -0
- package/dist/web/files.js.map +1 -0
- package/dist/web/history.d.ts +61 -0
- package/dist/web/history.d.ts.map +1 -0
- package/dist/web/history.js +197 -0
- package/dist/web/history.js.map +1 -0
- package/dist/web/mark.d.ts +15 -0
- package/dist/web/mark.d.ts.map +1 -0
- package/dist/web/mark.js +15 -0
- package/dist/web/mark.js.map +1 -0
- package/dist/web/search.d.ts +45 -0
- package/dist/web/search.d.ts.map +1 -0
- package/dist/web/search.js +147 -0
- package/dist/web/search.js.map +1 -0
- package/dist/web/sessions.d.ts +8 -0
- package/dist/web/sessions.d.ts.map +1 -1
- package/dist/web/sessions.js +9 -0
- package/dist/web/sessions.js.map +1 -1
- package/dist/web/ui-client.d.ts +14 -0
- package/dist/web/ui-client.d.ts.map +1 -0
- package/dist/web/ui-client.js +0 -0
- package/dist/web/ui-client.js.map +1 -0
- package/dist/web/ui-css.d.ts +15 -0
- package/dist/web/ui-css.d.ts.map +1 -0
- package/dist/web/ui-css.js +329 -0
- package/dist/web/ui-css.js.map +1 -0
- package/dist/web/ui.d.ts +6 -9
- package/dist/web/ui.d.ts.map +1 -1
- package/dist/web/ui.js +100 -541
- package/dist/web/ui.js.map +1 -1
- package/dist/workspace-trust.d.ts +8 -0
- package/dist/workspace-trust.d.ts.map +1 -1
- package/dist/workspace-trust.js +11 -0
- package/dist/workspace-trust.js.map +1 -1
- package/package.json +1 -1
package/dist/web/ui.js
CHANGED
|
@@ -1,230 +1,138 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The page, assembled.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
8
|
-
*
|
|
9
|
-
* Nothing is fetched from the network either. A page that pulls a font or a script from a CDN does
|
|
10
|
-
* not work on a machine that is offline, which is the machine most likely to be running a local
|
|
11
|
-
* model in the first place.
|
|
4
|
+
* One HTML document with its styles and script inline, served from memory. No build step and
|
|
5
|
+
* nothing fetched from the network: KONECK is installed with `npm i -g koneck`, so anything needing
|
|
6
|
+
* a second toolchain would be broken for most people on the day they first tried it — and a page
|
|
7
|
+
* that pulls a font from a CDN does not work on the machine most likely to be running a local
|
|
8
|
+
* model, which is one that is offline.
|
|
12
9
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<rect x="0" y="0" width="5" height="12"/><rect x="29" y="0" width="5" height="12"/>
|
|
17
|
-
<path d="M5 6 L11 0 L14 0 L7 7 Z"/><path d="M5 6 L11 12 L14 12 L7 5 Z"/>
|
|
18
|
-
<path d="M29 6 L23 0 L20 0 L27 7 Z"/><path d="M29 6 L23 12 L20 12 L27 5 Z"/>
|
|
19
|
-
</g></svg>`;
|
|
20
|
-
/** The mirrored K, as a character reference so no source file needs the glyph itself. */
|
|
21
|
-
const KK = '<span class="kk">Kꓘ</span>';
|
|
10
|
+
import { MARK_MASK } from './mark.js';
|
|
11
|
+
import { css } from './ui-css.js';
|
|
12
|
+
import { clientScript } from './ui-client.js';
|
|
22
13
|
export function page() {
|
|
23
14
|
return `<!doctype html>
|
|
24
15
|
<html lang="en"><head>
|
|
25
16
|
<meta charset="utf-8">
|
|
26
17
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
27
18
|
<meta name="referrer" content="no-referrer">
|
|
19
|
+
<meta name="color-scheme" content="light dark">
|
|
28
20
|
<title>KONECK</title>
|
|
29
|
-
<style>
|
|
30
|
-
:root{
|
|
31
|
-
--bg:#0b1013; --panel:#111a1f; --panel-2:#16232a; --line:#1e3038;
|
|
32
|
-
--ink:#d7e6ec; --muted:#7f9aa6; --dim:#4f6874;
|
|
33
|
-
--cyan:#4dd6e8; --amber:#e8b04d; --green:#57d99a; --red:#ef6d72; --violet:#a78bfa;
|
|
34
|
-
--radius:10px; --mono:ui-monospace,'SF Mono',Menlo,Consolas,'DejaVu Sans Mono',monospace;
|
|
35
|
-
}
|
|
36
|
-
*{box-sizing:border-box}
|
|
37
|
-
html,body{height:100%}
|
|
38
|
-
body{margin:0;background:var(--bg);color:var(--ink);
|
|
39
|
-
font:14px/1.55 ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
|
|
40
|
-
display:flex;overflow:hidden}
|
|
41
|
-
button,input,textarea,select{font:inherit;color:inherit}
|
|
42
|
-
::-webkit-scrollbar{width:9px;height:9px}
|
|
43
|
-
::-webkit-scrollbar-thumb{background:#24373f;border-radius:9px}
|
|
44
|
-
::-webkit-scrollbar-track{background:transparent}
|
|
45
|
-
.kk{color:var(--cyan);font-family:var(--mono)}
|
|
46
|
-
|
|
47
|
-
aside{width:264px;flex:0 0 264px;background:var(--panel);border-right:1px solid var(--line);
|
|
48
|
-
display:flex;flex-direction:column;min-height:0}
|
|
49
|
-
.brand{display:flex;align-items:center;gap:9px;padding:16px 16px 13px;color:var(--cyan)}
|
|
50
|
-
.mark{width:30px;height:11px;display:block}
|
|
51
|
-
.brand b{font-size:13px;letter-spacing:.16em;font-weight:600;color:var(--ink)}
|
|
52
|
-
.brand span{font-size:9px;letter-spacing:.14em;color:var(--dim);
|
|
53
|
-
border:1px solid var(--line);border-radius:4px;padding:2px 5px}
|
|
54
|
-
.newbtn{margin:0 12px 14px;padding:9px;border:1px solid var(--line);background:var(--panel-2);
|
|
55
|
-
border-radius:var(--radius);color:var(--ink);cursor:pointer;display:flex;align-items:center;
|
|
56
|
-
justify-content:center;gap:7px;transition:.14s}
|
|
57
|
-
.newbtn:hover{border-color:var(--cyan);color:var(--cyan)}
|
|
58
|
-
.shead{padding:6px 16px;font-size:10px;letter-spacing:.13em;color:var(--dim);text-transform:uppercase}
|
|
59
|
-
.slist{overflow-y:auto;flex:1;min-height:0;padding:0 8px 10px}
|
|
60
|
-
.srow{padding:8px 10px;border-radius:8px;cursor:pointer;margin-bottom:2px;border:1px solid transparent}
|
|
61
|
-
.srow:hover{background:var(--panel-2)}
|
|
62
|
-
.srow.on{background:var(--panel-2);border-color:var(--line)}
|
|
63
|
-
.srow .t{font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
64
|
-
.srow .m{font-size:10.5px;color:var(--dim);margin-top:2px;display:flex;gap:7px;align-items:center}
|
|
65
|
-
.dot{width:6px;height:6px;border-radius:50%;background:var(--green);flex:0 0 6px}
|
|
66
|
-
.dot.busy{background:var(--amber);animation:pulse 1.1s infinite}
|
|
67
|
-
@keyframes pulse{50%{opacity:.28}}
|
|
68
|
-
.foot{border-top:1px solid var(--line);padding:10px 14px;font-size:10.5px;color:var(--dim);
|
|
69
|
-
display:flex;justify-content:space-between;align-items:center}
|
|
70
|
-
|
|
71
|
-
main{flex:1;display:flex;flex-direction:column;min-width:0;min-height:0}
|
|
72
|
-
header{height:46px;flex:0 0 46px;border-bottom:1px solid var(--line);display:flex;
|
|
73
|
-
align-items:center;gap:10px;padding:0 16px;background:var(--panel);font-size:12px}
|
|
74
|
-
.pill{border:1px solid var(--line);background:var(--panel-2);border-radius:999px;
|
|
75
|
-
padding:4px 11px;color:var(--muted);display:flex;gap:6px;align-items:center;cursor:pointer}
|
|
76
|
-
.pill:hover{border-color:var(--cyan);color:var(--ink)}
|
|
77
|
-
.pill b{color:var(--ink);font-weight:500}
|
|
78
|
-
.spacer{flex:1}
|
|
79
|
-
.stat{color:var(--dim);font-variant-numeric:tabular-nums}
|
|
80
|
-
.stat b{color:var(--muted);font-weight:500}
|
|
81
|
-
|
|
82
|
-
#feed{flex:1;overflow-y:auto;padding:26px 0 8px;min-height:0}
|
|
83
|
-
.wrap{max-width:820px;margin:0 auto;padding:0 24px}
|
|
84
|
-
.hero{text-align:center;padding:13vh 0 0;color:var(--muted)}
|
|
85
|
-
.hero .big{display:flex;align-items:center;justify-content:center;gap:12px;color:var(--cyan);
|
|
86
|
-
margin-bottom:8px}
|
|
87
|
-
.hero .big .mark{width:52px;height:19px}
|
|
88
|
-
.hero h1{font-size:21px;margin:0;color:var(--ink);font-weight:600;letter-spacing:.01em}
|
|
89
|
-
.hero p{font-size:13px;margin:7px 0 0}
|
|
90
|
-
|
|
91
|
-
.msg{margin-bottom:20px}
|
|
92
|
-
.you{background:var(--panel-2);border:1px solid var(--line);border-radius:var(--radius);
|
|
93
|
-
padding:11px 14px;white-space:pre-wrap;word-break:break-word}
|
|
94
|
-
.who{display:flex;align-items:center;gap:8px;font-size:10.5px;letter-spacing:.12em;
|
|
95
|
-
color:var(--dim);text-transform:uppercase;margin-bottom:7px}
|
|
96
|
-
.reply{white-space:pre-wrap;word-break:break-word}
|
|
97
|
-
.reply code{background:var(--panel-2);border:1px solid var(--line);border-radius:4px;
|
|
98
|
-
padding:1px 5px;font-family:var(--mono);font-size:12.5px}
|
|
99
|
-
.reply pre{background:var(--panel);border:1px solid var(--line);border-radius:8px;
|
|
100
|
-
padding:12px 14px;overflow-x:auto;margin:10px 0;white-space:pre}
|
|
101
|
-
.reply pre code{background:none;border:0;padding:0;font-size:12.5px;line-height:1.5}
|
|
102
|
-
.reply h3{font-size:14px;margin:16px 0 6px}
|
|
103
|
-
.reply strong{color:#eaf4f8}
|
|
104
|
-
.reply table{border-collapse:collapse;margin:10px 0;font-size:12.5px;display:block;overflow-x:auto}
|
|
105
|
-
.reply th,.reply td{border:1px solid var(--line);padding:5px 9px;text-align:left}
|
|
106
|
-
.reply th{background:var(--panel-2);color:var(--muted)}
|
|
107
|
-
|
|
108
|
-
.tool{border:1px solid var(--line);border-left:2px solid var(--dim);border-radius:8px;
|
|
109
|
-
background:var(--panel);margin:8px 0;overflow:hidden}
|
|
110
|
-
.tool.ok{border-left-color:var(--green)} .tool.bad{border-left-color:var(--red)}
|
|
111
|
-
.tool.run{border-left-color:var(--amber)}
|
|
112
|
-
.tool .th{display:flex;align-items:center;gap:9px;padding:8px 12px;cursor:pointer;font-size:12.5px}
|
|
113
|
-
.tool .nm{font-family:var(--mono);color:var(--cyan)}
|
|
114
|
-
.tool .dt{color:var(--muted);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
115
|
-
.tool .ms{color:var(--dim);font-variant-numeric:tabular-nums;font-size:11px}
|
|
116
|
-
.tool .body{border-top:1px solid var(--line);padding:9px 12px;font-family:var(--mono);
|
|
117
|
-
font-size:11.5px;color:var(--muted);white-space:pre-wrap;max-height:280px;overflow:auto}
|
|
118
|
-
.think{border:1px dashed var(--line);border-radius:8px;padding:9px 12px;margin:8px 0;
|
|
119
|
-
color:var(--violet);font-size:12.5px;white-space:pre-wrap;max-height:200px;overflow:auto}
|
|
120
|
-
.notice{border:1px solid var(--line);border-left:2px solid var(--amber);border-radius:8px;
|
|
121
|
-
padding:10px 13px;margin:9px 0;font-size:12.5px;color:var(--muted);white-space:pre-wrap}
|
|
122
|
-
.notice.info{border-left-color:var(--cyan)}
|
|
123
|
-
.err{border:1px solid #4a2226;border-left:2px solid var(--red);background:#180f11;
|
|
124
|
-
border-radius:8px;padding:10px 13px;margin:9px 0;font-size:12.5px;color:#f4b8ba;white-space:pre-wrap}
|
|
125
|
-
|
|
126
|
-
.plan{border:1px solid var(--line);border-radius:8px;background:var(--panel);margin:9px 0;
|
|
127
|
-
padding:10px 13px;font-size:12.5px}
|
|
128
|
-
.plan h4{margin:0 0 7px;font-size:10.5px;letter-spacing:.12em;color:var(--dim);
|
|
129
|
-
text-transform:uppercase;display:flex;gap:8px}
|
|
130
|
-
.step{display:flex;gap:9px;padding:2px 0;color:var(--muted)}
|
|
131
|
-
.step .bx{width:13px;color:var(--dim);flex:0 0 13px}
|
|
132
|
-
.step.done{color:var(--dim)} .step.done .bx{color:var(--green)}
|
|
133
|
-
.step.doing{color:var(--ink)} .step.doing .bx{color:var(--amber)}
|
|
134
|
-
.step.blocked .bx{color:var(--red)}
|
|
135
|
-
|
|
136
|
-
.ask{border:1px solid var(--amber);border-radius:var(--radius);background:#1a1508;
|
|
137
|
-
padding:12px 14px;margin:11px 0}
|
|
138
|
-
.ask .q{font-size:13px;margin-bottom:4px}
|
|
139
|
-
.ask .a{font-family:var(--mono);font-size:11.5px;color:var(--muted);margin-bottom:10px;
|
|
140
|
-
white-space:pre-wrap;max-height:130px;overflow:auto}
|
|
141
|
-
.ask .btns{display:flex;gap:8px}
|
|
142
|
-
.ask button{padding:6px 14px;border-radius:7px;border:1px solid var(--line);cursor:pointer;
|
|
143
|
-
background:var(--panel-2)}
|
|
144
|
-
.ask .yes{border-color:var(--green);color:var(--green)}
|
|
145
|
-
.ask .no{border-color:var(--red);color:var(--red)}
|
|
146
|
-
|
|
147
|
-
.busy{display:flex;align-items:center;gap:10px;color:var(--amber);font-size:12.5px;margin:8px 0}
|
|
148
|
-
.sp{width:11px;height:11px;border:2px solid var(--amber);border-right-color:transparent;
|
|
149
|
-
border-radius:50%;animation:spin .8s linear infinite}
|
|
150
|
-
@keyframes spin{to{transform:rotate(360deg)}}
|
|
151
|
-
|
|
152
|
-
footer{flex:0 0 auto;border-top:1px solid var(--line);background:var(--panel);padding:12px 0 14px}
|
|
153
|
-
.cbox{max-width:820px;margin:0 auto;padding:0 24px}
|
|
154
|
-
.cin{display:flex;align-items:flex-end;gap:10px;border:1px solid var(--line);
|
|
155
|
-
background:var(--panel-2);border-radius:var(--radius);padding:9px 11px;transition:.14s}
|
|
156
|
-
.cin:focus-within{border-color:var(--cyan)}
|
|
157
|
-
textarea{flex:1;background:none;border:0;outline:0;resize:none;max-height:190px;
|
|
158
|
-
line-height:1.5;font-size:14px}
|
|
159
|
-
textarea::placeholder{color:var(--dim)}
|
|
160
|
-
.send{width:31px;height:31px;flex:0 0 31px;border-radius:50%;border:0;cursor:pointer;
|
|
161
|
-
background:var(--cyan);color:#04181c;font-size:15px;display:grid;place-items:center}
|
|
162
|
-
.send:disabled{background:var(--line);color:var(--dim);cursor:not-allowed}
|
|
163
|
-
.chint{display:flex;gap:12px;margin-top:7px;font-size:10.5px;color:var(--dim)}
|
|
164
|
-
.chint kbd{border:1px solid var(--line);border-radius:4px;padding:0 4px;font-family:var(--mono)}
|
|
165
|
-
|
|
166
|
-
dialog{border:1px solid var(--line);background:var(--panel);color:var(--ink);
|
|
167
|
-
border-radius:var(--radius);padding:0;max-width:440px;width:92%}
|
|
168
|
-
dialog::backdrop{background:rgba(0,0,0,.66)}
|
|
169
|
-
.dlg{padding:18px}
|
|
170
|
-
.dlg h3{margin:0 0 4px;font-size:15px}
|
|
171
|
-
.dlg p{margin:0 0 14px;color:var(--muted);font-size:12.5px}
|
|
172
|
-
.dlg input,.dlg select{width:100%;padding:8px 10px;border-radius:8px;border:1px solid var(--line);
|
|
173
|
-
background:var(--panel-2);margin-bottom:10px}
|
|
174
|
-
.dlg .row{display:flex;gap:8px;justify-content:flex-end}
|
|
175
|
-
.dlg button{padding:7px 15px;border-radius:8px;border:1px solid var(--line);cursor:pointer;
|
|
176
|
-
background:var(--panel-2)}
|
|
177
|
-
.dlg .go{border-color:var(--cyan);color:var(--cyan)}
|
|
178
|
-
</style>
|
|
21
|
+
<style>${css(MARK_MASK)}</style>
|
|
179
22
|
</head><body>
|
|
180
23
|
|
|
181
|
-
<aside>
|
|
182
|
-
<div class="brand"
|
|
183
|
-
|
|
184
|
-
|
|
24
|
+
<aside id="side">
|
|
25
|
+
<div class="brand">
|
|
26
|
+
<div class="mark"></div>
|
|
27
|
+
<b>KONECK</b>
|
|
28
|
+
<span class="tag">web</span>
|
|
29
|
+
</div>
|
|
30
|
+
<button class="newbtn" id="new">New session</button>
|
|
185
31
|
<div class="slist" id="slist"></div>
|
|
186
|
-
<div class="foot"
|
|
32
|
+
<div class="foot">
|
|
33
|
+
<span id="wsname" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">—</span>
|
|
34
|
+
<button class="themebtn" id="theme">system</button>
|
|
35
|
+
<span id="conn">offline</span>
|
|
36
|
+
</div>
|
|
187
37
|
</aside>
|
|
188
38
|
|
|
189
39
|
<main>
|
|
190
40
|
<header>
|
|
191
|
-
<
|
|
192
|
-
<div class="
|
|
41
|
+
<button class="iconbtn" id="sidebtn" title="Show or hide the sidebar">☰</button>
|
|
42
|
+
<div class="tabs">
|
|
43
|
+
<button class="tab on" id="tab-chat">Chat</button>
|
|
44
|
+
<button class="tab" id="tab-changes">Changes</button>
|
|
45
|
+
<button class="tab" id="tab-timing">Timing</button>
|
|
46
|
+
<button class="tab" id="tab-files">Files</button>
|
|
47
|
+
<button class="tab" id="tab-search">Search</button>
|
|
48
|
+
</div>
|
|
193
49
|
<div class="spacer"></div>
|
|
194
|
-
<
|
|
195
|
-
<
|
|
50
|
+
<button class="pill" id="pws" title="Switch workspace">workspace <b>—</b></button>
|
|
51
|
+
<button class="pill" id="pprov">provider <b>—</b></button>
|
|
52
|
+
<button class="pill" id="pmodel">model <b>—</b></button>
|
|
53
|
+
<div class="stat" id="stat">—</div>
|
|
54
|
+
<button class="stopbtn" id="stop" hidden>stop</button>
|
|
196
55
|
</header>
|
|
197
56
|
|
|
198
|
-
<div
|
|
199
|
-
<div class="
|
|
200
|
-
<div class="
|
|
201
|
-
|
|
57
|
+
<div class="view" id="chatview">
|
|
58
|
+
<div class="wrap" id="stream">
|
|
59
|
+
<div class="hero">
|
|
60
|
+
<div class="mark"></div>
|
|
61
|
+
<h1>Into the workspace</h1>
|
|
62
|
+
<p>Everything the CLI does, from here. Ask for a change and watch it happen.</p>
|
|
63
|
+
<div class="ex">
|
|
64
|
+
<button>What is in this project?</button>
|
|
65
|
+
<button>Find the slowest test and explain why</button>
|
|
66
|
+
<button>Add a README with real usage examples</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
202
69
|
</div>
|
|
203
|
-
</div
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="view" id="changesview" hidden></div>
|
|
73
|
+
<div class="view" id="timingview" hidden></div>
|
|
204
74
|
|
|
205
|
-
<
|
|
206
|
-
<div class="
|
|
207
|
-
<
|
|
208
|
-
<
|
|
75
|
+
<div class="view" id="filesview" hidden>
|
|
76
|
+
<div class="split">
|
|
77
|
+
<div class="tree" id="tree"></div>
|
|
78
|
+
<div class="fileview" id="fileview"></div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="view" id="searchview" hidden>
|
|
83
|
+
<div class="wrap">
|
|
84
|
+
<div class="searchbar">
|
|
85
|
+
<input id="q" placeholder="Search everything KONECK has done here…" autocomplete="off">
|
|
86
|
+
<button id="qgo">Search</button>
|
|
87
|
+
</div>
|
|
88
|
+
<div id="qresults"></div>
|
|
209
89
|
</div>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<footer id="composer">
|
|
93
|
+
<div class="cbox">
|
|
94
|
+
<div class="cin">
|
|
95
|
+
<textarea id="ta" rows="1" placeholder="Describe what you want to build…"></textarea>
|
|
96
|
+
<button class="send" id="send" disabled title="Send">↑</button>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="chint">
|
|
99
|
+
<span><kbd>Enter</kbd> send</span>
|
|
100
|
+
<span><kbd>Shift</kbd>+<kbd>Enter</kbd> newline</span>
|
|
101
|
+
</div>
|
|
213
102
|
</div>
|
|
214
|
-
</
|
|
103
|
+
</footer>
|
|
104
|
+
<footer id="readonlybar" hidden>
|
|
105
|
+
<div class="readonly">
|
|
106
|
+
<span>Finished session, read only —</span>
|
|
107
|
+
<b id="readonlywhat" style="font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></b>
|
|
108
|
+
</div>
|
|
109
|
+
</footer>
|
|
215
110
|
</main>
|
|
216
111
|
|
|
217
112
|
<dialog id="tokdlg"><form class="dlg" method="dialog">
|
|
218
113
|
<h3>Token needed</h3>
|
|
219
114
|
<p>Anything that starts a run needs the token KONECK printed when it started. It is in the
|
|
220
115
|
terminal you launched it from.</p>
|
|
221
|
-
<
|
|
116
|
+
<label for="tokin">Token</label>
|
|
117
|
+
<input id="tokin" placeholder="paste it here" autocomplete="off">
|
|
222
118
|
<div class="row"><button class="go" id="toksave">Use it</button></div>
|
|
223
119
|
</form></dialog>
|
|
224
120
|
|
|
121
|
+
<dialog id="wsdlg"><form class="dlg" method="dialog">
|
|
122
|
+
<h3>Workspace</h3>
|
|
123
|
+
<p>Only workspaces you have already trusted are listed. Trust is granted at a terminal, after
|
|
124
|
+
being asked — so a browser cannot add to this list.</p>
|
|
125
|
+
<label for="wschoice">Open</label>
|
|
126
|
+
<select id="wschoice"></select>
|
|
127
|
+
<div class="row"><button>Cancel</button><button class="go" id="wssave">Switch</button></div>
|
|
128
|
+
</form></dialog>
|
|
129
|
+
|
|
225
130
|
<dialog id="mdlg"><form class="dlg" method="dialog">
|
|
226
|
-
<h3>Model</h3
|
|
131
|
+
<h3>Model</h3>
|
|
132
|
+
<p>Applies from your next message.</p>
|
|
133
|
+
<label for="mprov">Provider</label>
|
|
227
134
|
<select id="mprov"></select>
|
|
135
|
+
<label for="mmodel">Model</label>
|
|
228
136
|
<input id="mmodel" placeholder="model name" autocomplete="off">
|
|
229
137
|
<div class="row"><button>Cancel</button><button class="go" id="msave">Switch</button></div>
|
|
230
138
|
</form></dialog>
|
|
@@ -234,353 +142,4 @@ ${clientScript()}
|
|
|
234
142
|
</script>
|
|
235
143
|
</body></html>`;
|
|
236
144
|
}
|
|
237
|
-
/**
|
|
238
|
-
* The client.
|
|
239
|
-
*
|
|
240
|
-
* Plain DOM. A framework would be more code than the page it renders, and this has one job: turn a
|
|
241
|
-
* stream of typed events into a transcript that reads well.
|
|
242
|
-
*/
|
|
243
|
-
function clientScript() {
|
|
244
|
-
return String.raw `
|
|
245
|
-
const $ = (id) => document.getElementById(id);
|
|
246
|
-
const state = {
|
|
247
|
-
token: new URLSearchParams(location.search).get('token') || localStorage.getItem('koneck.token') || '',
|
|
248
|
-
session: null, provider: '-', model: '-', busy: false, es: null,
|
|
249
|
-
// The element currently being appended to, so streamed text lands in one bubble rather than
|
|
250
|
-
// producing a new one per chunk.
|
|
251
|
-
replyEl: null, thinkEl: null, toolEls: new Map(), planEl: null, lastTool: null,
|
|
252
|
-
};
|
|
253
|
-
if (state.token) localStorage.setItem('koneck.token', state.token);
|
|
254
|
-
// The token does not belong in the address bar, where it survives in history and in a screenshot.
|
|
255
|
-
if (location.search.indexOf('token=') !== -1) history.replaceState({}, '', location.pathname);
|
|
256
|
-
|
|
257
|
-
const api = async (path, opts) => {
|
|
258
|
-
opts = opts || {};
|
|
259
|
-
const sep = path.indexOf('?') === -1 ? '?' : '&';
|
|
260
|
-
const res = await fetch(path + sep + 'token=' + encodeURIComponent(state.token), {
|
|
261
|
-
method: opts.method || 'GET',
|
|
262
|
-
body: opts.body,
|
|
263
|
-
headers: { 'content-type': 'application/json' },
|
|
264
|
-
});
|
|
265
|
-
if (res.status === 401) { $('tokdlg').showModal(); throw new Error('token required'); }
|
|
266
|
-
if (!res.ok) {
|
|
267
|
-
let msg = res.statusText;
|
|
268
|
-
try { msg = (await res.json()).error || msg; } catch (e) {}
|
|
269
|
-
throw new Error(msg);
|
|
270
|
-
}
|
|
271
|
-
return res.status === 204 ? null : res.json();
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
const stream = () => $('stream');
|
|
275
|
-
const atBottom = () => {
|
|
276
|
-
const f = $('feed');
|
|
277
|
-
return f.scrollHeight - f.scrollTop - f.clientHeight < 90;
|
|
278
|
-
};
|
|
279
|
-
const stick = (was) => { if (was) $('feed').scrollTop = $('feed').scrollHeight; };
|
|
280
|
-
const el = (tag, cls, text) => {
|
|
281
|
-
const n = document.createElement(tag);
|
|
282
|
-
if (cls) n.className = cls;
|
|
283
|
-
if (text !== undefined) n.textContent = text;
|
|
284
|
-
return n;
|
|
285
|
-
};
|
|
286
|
-
const add = (node) => { const was = atBottom(); stream().appendChild(node); stick(was); return node; };
|
|
287
|
-
const clearHero = () => { const h = stream().querySelector('.hero'); if (h) h.remove(); };
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Enough markdown to read a reply: fences, inline code, bold, headings, tables.
|
|
291
|
-
*
|
|
292
|
-
* Everything is escaped before any tag is inserted, and fenced blocks are lifted out first so
|
|
293
|
-
* their contents can never be interpreted. Model output is untrusted text — it routinely contains
|
|
294
|
-
* HTML, because it is often writing HTML.
|
|
295
|
-
*/
|
|
296
|
-
function md(src) {
|
|
297
|
-
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
298
|
-
const blocks = [];
|
|
299
|
-
let text = src.replace(/` + '```' + String.raw `([a-zA-Z0-9_+-]*)\n([\s\S]*?)` + '```' + String.raw `/g, function (m, lang, code) {
|
|
300
|
-
blocks.push('<pre><code>' + esc(code.replace(/\n$/, '')) + '</code></pre>');
|
|
301
|
-
return '' + (blocks.length - 1) + '';
|
|
302
|
-
});
|
|
303
|
-
text = esc(text);
|
|
304
|
-
text = text.replace(/(^\|.+\|\n\|[ :|-]+\|\n(?:\|.*\|\n?)*)/gm, function (t) {
|
|
305
|
-
const rows = t.trim().split('\n').filter(function (r) { return !/^\|[ :|-]+\|$/.test(r.trim()); });
|
|
306
|
-
const cells = (r, tag) => '<tr>' + r.split('|').slice(1, -1)
|
|
307
|
-
.map((c) => '<' + tag + '>' + c.trim() + '</' + tag + '>').join('') + '</tr>';
|
|
308
|
-
return '<table>' + cells(rows[0], 'th') +
|
|
309
|
-
rows.slice(1).map((r) => cells(r, 'td')).join('') + '</table>';
|
|
310
|
-
});
|
|
311
|
-
text = text
|
|
312
|
-
.replace(/^#{2,3} (.*)$/gm, '<h3>$1</h3>')
|
|
313
|
-
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
314
|
-
.replace(/` + '`' + String.raw `([^` + '`' + String.raw `\n]+)` + '`' + String.raw `/g, '<code>$1</code>');
|
|
315
|
-
return text.replace(/(\d+)/g, (m, i) => blocks[Number(i)]);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const ms = (n) => n >= 1000 ? (n / 1000).toFixed(n >= 10000 ? 0 : 1) + 's' : Math.round(n) + 'ms';
|
|
319
|
-
const num = (n) => n.toLocaleString();
|
|
320
|
-
|
|
321
|
-
function renderTool(e) {
|
|
322
|
-
const box = el('div', 'tool run');
|
|
323
|
-
const th = el('div', 'th');
|
|
324
|
-
th.appendChild(el('span', 'nm', e.name));
|
|
325
|
-
th.appendChild(el('span', 'dt', e.detail || ''));
|
|
326
|
-
th.appendChild(el('span', 'ms', '...'));
|
|
327
|
-
const body = el('div', 'body');
|
|
328
|
-
body.style.display = 'none';
|
|
329
|
-
th.onclick = () => { body.style.display = body.style.display === 'none' ? 'block' : 'none'; };
|
|
330
|
-
box.appendChild(th); box.appendChild(body);
|
|
331
|
-
const entry = { box: box, th: th, body: body };
|
|
332
|
-
state.toolEls.set(e.callId, entry);
|
|
333
|
-
state.lastTool = entry;
|
|
334
|
-
return add(box);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function finishTool(e) {
|
|
338
|
-
const found = state.toolEls.get(e.callId) || state.lastTool;
|
|
339
|
-
if (!found) return;
|
|
340
|
-
found.box.className = 'tool ' + (e.ok ? 'ok' : 'bad');
|
|
341
|
-
found.th.children[2].textContent = ms(e.ms);
|
|
342
|
-
if (e.detail) found.th.children[1].textContent = e.detail;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function renderPlan(steps) {
|
|
346
|
-
const done = steps.filter((s) => s.state === 'done').length;
|
|
347
|
-
const box = el('div', 'plan');
|
|
348
|
-
const h = el('h4');
|
|
349
|
-
h.appendChild(el('span', null, 'Plan'));
|
|
350
|
-
h.appendChild(el('span', null, done + '/' + steps.length));
|
|
351
|
-
box.appendChild(h);
|
|
352
|
-
for (const s of steps) {
|
|
353
|
-
const row = el('div', 'step ' + s.state);
|
|
354
|
-
const mark = s.state === 'done' ? 'v' : s.state === 'doing' ? '>' : s.state === 'blocked' ? 'x' : '.';
|
|
355
|
-
row.appendChild(el('span', 'bx', mark));
|
|
356
|
-
row.appendChild(el('span', null, s.text));
|
|
357
|
-
box.appendChild(row);
|
|
358
|
-
}
|
|
359
|
-
if (state.planEl && state.planEl.parentNode) {
|
|
360
|
-
state.planEl.replaceWith(box); state.planEl = box; return box;
|
|
361
|
-
}
|
|
362
|
-
state.planEl = box;
|
|
363
|
-
return add(box);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function renderAsk(e) {
|
|
367
|
-
const box = el('div', 'ask');
|
|
368
|
-
box.appendChild(el('div', 'q', 'Allow ' + e.tool + '?'));
|
|
369
|
-
box.appendChild(el('div', 'a', e.args));
|
|
370
|
-
const btns = el('div', 'btns');
|
|
371
|
-
const answer = async (allow) => {
|
|
372
|
-
const all = btns.querySelectorAll('button');
|
|
373
|
-
for (let i = 0; i < all.length; i++) all[i].disabled = true;
|
|
374
|
-
await api('/api/answer', { method: 'POST',
|
|
375
|
-
body: JSON.stringify({ session: state.session, ask: e.askId, allow: allow }) });
|
|
376
|
-
box.appendChild(el('div', 'a', allow ? 'allowed' : 'refused'));
|
|
377
|
-
};
|
|
378
|
-
const yes = el('button', 'yes', 'Allow'); yes.onclick = () => answer(true);
|
|
379
|
-
const no = el('button', 'no', 'Refuse'); no.onclick = () => answer(false);
|
|
380
|
-
btns.appendChild(yes); btns.appendChild(no);
|
|
381
|
-
box.appendChild(btns);
|
|
382
|
-
return add(box);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
let spinner = null;
|
|
386
|
-
function setBusy(on) {
|
|
387
|
-
state.busy = on;
|
|
388
|
-
$('send').disabled = on || !$('ta').value.trim();
|
|
389
|
-
$('pabort').style.display = on ? 'flex' : 'none';
|
|
390
|
-
$('hintstate').textContent = on ? 'working...' : '';
|
|
391
|
-
if (on && !spinner) {
|
|
392
|
-
spinner = el('div', 'busy');
|
|
393
|
-
spinner.appendChild(el('div', 'sp'));
|
|
394
|
-
spinner.appendChild(el('span', null, 'working'));
|
|
395
|
-
add(spinner);
|
|
396
|
-
} else if (!on && spinner) { spinner.remove(); spinner = null; }
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function onEvent(e) {
|
|
400
|
-
switch (e.t) {
|
|
401
|
-
case 'session':
|
|
402
|
-
state.provider = e.provider; state.model = e.model;
|
|
403
|
-
setPills(e.provider, e.model);
|
|
404
|
-
$('wsname').textContent = e.cwd.split('/').pop() || e.cwd;
|
|
405
|
-
break;
|
|
406
|
-
case 'user': {
|
|
407
|
-
clearHero();
|
|
408
|
-
const box = el('div', 'msg');
|
|
409
|
-
box.appendChild(el('div', 'who', 'you'));
|
|
410
|
-
box.appendChild(el('div', 'you', e.text));
|
|
411
|
-
add(box); state.replyEl = null; state.thinkEl = null;
|
|
412
|
-
setBusy(true);
|
|
413
|
-
break;
|
|
414
|
-
}
|
|
415
|
-
case 'chunk': {
|
|
416
|
-
if (!state.replyEl) {
|
|
417
|
-
const box = el('div', 'msg');
|
|
418
|
-
const who = el('div', 'who');
|
|
419
|
-
who.appendChild(el('span', 'kk', 'koneck'));
|
|
420
|
-
const body = el('div', 'reply');
|
|
421
|
-
box.appendChild(who); box.appendChild(body); add(box);
|
|
422
|
-
state.replyEl = body; state.replyEl.dataset.raw = '';
|
|
423
|
-
}
|
|
424
|
-
state.replyEl.dataset.raw += e.text;
|
|
425
|
-
const was = atBottom();
|
|
426
|
-
state.replyEl.innerHTML = md(state.replyEl.dataset.raw);
|
|
427
|
-
stick(was);
|
|
428
|
-
break;
|
|
429
|
-
}
|
|
430
|
-
case 'thinking':
|
|
431
|
-
if (!state.thinkEl) state.thinkEl = add(el('div', 'think', ''));
|
|
432
|
-
state.thinkEl.textContent = (state.thinkEl.textContent + e.text).slice(-1400);
|
|
433
|
-
break;
|
|
434
|
-
case 'tool': clearHero(); renderTool(e); break;
|
|
435
|
-
case 'tool-output': {
|
|
436
|
-
const any = state.lastTool;
|
|
437
|
-
if (any) any.body.textContent = (any.body.textContent + e.line + '\n').slice(-4000);
|
|
438
|
-
break;
|
|
439
|
-
}
|
|
440
|
-
case 'tool-done': finishTool(e); break;
|
|
441
|
-
case 'plan': renderPlan(e.steps); break;
|
|
442
|
-
case 'tokens':
|
|
443
|
-
$('stok').innerHTML = '<b>' + num(e.total) + '</b> tokens' +
|
|
444
|
-
(e.cost > 0 ? ' · $' + e.cost.toFixed(4) : '');
|
|
445
|
-
break;
|
|
446
|
-
case 'ask': renderAsk(e); break;
|
|
447
|
-
case 'notice': add(el('div', 'notice ' + (e.level === 'info' ? 'info' : ''), e.text)); break;
|
|
448
|
-
case 'error': add(el('div', 'err', e.text)); setBusy(false); break;
|
|
449
|
-
case 'turn-end':
|
|
450
|
-
setBusy(false);
|
|
451
|
-
state.replyEl = null; state.thinkEl = null;
|
|
452
|
-
break;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function setPills(provider, model) {
|
|
457
|
-
$('pprov').innerHTML = 'provider <b>' + escHtml(provider) + '</b>';
|
|
458
|
-
$('pmodel').innerHTML = 'model <b>' + escHtml(model) + '</b>';
|
|
459
|
-
}
|
|
460
|
-
function escHtml(s) {
|
|
461
|
-
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function listen() {
|
|
465
|
-
if (state.es) state.es.close();
|
|
466
|
-
const es = new EventSource('/api/events?session=' + encodeURIComponent(state.session) +
|
|
467
|
-
'&token=' + encodeURIComponent(state.token));
|
|
468
|
-
state.es = es;
|
|
469
|
-
es.onopen = () => { $('conn').textContent = 'live'; $('conn').style.color = 'var(--green)'; };
|
|
470
|
-
es.onerror = () => { $('conn').textContent = 'reconnecting'; $('conn').style.color = 'var(--amber)'; };
|
|
471
|
-
es.onmessage = (m) => { try { onEvent(JSON.parse(m.data)); } catch (err) {} };
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
async function refreshSessions() {
|
|
475
|
-
let data = null;
|
|
476
|
-
try { data = await api('/api/sessions'); } catch (e) { return; }
|
|
477
|
-
const list = $('slist');
|
|
478
|
-
list.innerHTML = '';
|
|
479
|
-
for (const s of data.live) {
|
|
480
|
-
const row = el('div', 'srow' + (s.id === state.session ? ' on' : ''));
|
|
481
|
-
row.appendChild(el('div', 't', s.title));
|
|
482
|
-
const meta = el('div', 'm');
|
|
483
|
-
meta.appendChild(el('span', 'dot' + (s.busy ? ' busy' : '')));
|
|
484
|
-
meta.appendChild(el('span', null, s.model));
|
|
485
|
-
meta.appendChild(el('span', null, s.turns + ' turns'));
|
|
486
|
-
row.appendChild(meta);
|
|
487
|
-
row.onclick = () => { openSession(s.id); };
|
|
488
|
-
list.appendChild(row);
|
|
489
|
-
}
|
|
490
|
-
if (!data.live.length) list.appendChild(el('div', 'shead', 'none yet'));
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function resetView() {
|
|
494
|
-
stream().innerHTML = '';
|
|
495
|
-
state.planEl = null; state.replyEl = null; state.thinkEl = null; state.lastTool = null;
|
|
496
|
-
state.toolEls = new Map();
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function openSession(id) {
|
|
500
|
-
state.session = id;
|
|
501
|
-
resetView();
|
|
502
|
-
listen();
|
|
503
|
-
refreshSessions();
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
async function newSession() {
|
|
507
|
-
const s = await api('/api/sessions', { method: 'POST', body: JSON.stringify({}) });
|
|
508
|
-
openSession(s.id);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
async function send() {
|
|
512
|
-
const ta = $('ta');
|
|
513
|
-
const text = ta.value.trim();
|
|
514
|
-
if (!text || state.busy) return;
|
|
515
|
-
if (!state.session) await newSession();
|
|
516
|
-
ta.value = ''; ta.style.height = 'auto';
|
|
517
|
-
$('send').disabled = true;
|
|
518
|
-
try {
|
|
519
|
-
await api('/api/send', { method: 'POST',
|
|
520
|
-
body: JSON.stringify({ session: state.session, text: text }) });
|
|
521
|
-
} catch (e) { add(el('div', 'err', e.message)); setBusy(false); }
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
$('ta').addEventListener('input', (ev) => {
|
|
525
|
-
const ta = ev.target;
|
|
526
|
-
ta.style.height = 'auto';
|
|
527
|
-
ta.style.height = Math.min(ta.scrollHeight, 190) + 'px';
|
|
528
|
-
$('send').disabled = state.busy || !ta.value.trim();
|
|
529
|
-
});
|
|
530
|
-
$('ta').addEventListener('keydown', (ev) => {
|
|
531
|
-
if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); send(); }
|
|
532
|
-
});
|
|
533
|
-
$('send').onclick = send;
|
|
534
|
-
$('new').onclick = newSession;
|
|
535
|
-
$('pabort').onclick = () => {
|
|
536
|
-
api('/api/abort', { method: 'POST', body: JSON.stringify({ session: state.session }) })
|
|
537
|
-
.catch(() => {});
|
|
538
|
-
};
|
|
539
|
-
$('toksave').onclick = () => {
|
|
540
|
-
const v = $('tokin').value.trim();
|
|
541
|
-
if (v) { localStorage.setItem('koneck.token', v); location.reload(); }
|
|
542
|
-
};
|
|
543
|
-
$('pmodel').onclick = async () => {
|
|
544
|
-
let info = null;
|
|
545
|
-
try { info = await api('/api/workspaces'); } catch (e) { return; }
|
|
546
|
-
const sel = $('mprov');
|
|
547
|
-
sel.innerHTML = '';
|
|
548
|
-
for (const p of info.providers) {
|
|
549
|
-
const o = document.createElement('option');
|
|
550
|
-
o.value = p.name;
|
|
551
|
-
o.textContent = p.label + (p.keyless ? ' - local' : '');
|
|
552
|
-
if (p.name === state.provider) o.selected = true;
|
|
553
|
-
sel.appendChild(o);
|
|
554
|
-
}
|
|
555
|
-
$('mmodel').value = state.model === '-' ? info.model : state.model;
|
|
556
|
-
$('mdlg').showModal();
|
|
557
|
-
};
|
|
558
|
-
$('msave').onclick = async () => {
|
|
559
|
-
const provider = $('mprov').value, model = $('mmodel').value.trim();
|
|
560
|
-
if (!state.session) await newSession();
|
|
561
|
-
let info = null;
|
|
562
|
-
try {
|
|
563
|
-
info = await api('/api/model', { method: 'POST',
|
|
564
|
-
body: JSON.stringify({ session: state.session, provider: provider, model: model }) });
|
|
565
|
-
} catch (e) { return; }
|
|
566
|
-
state.provider = info.provider; state.model = info.model;
|
|
567
|
-
setPills(info.provider, info.model);
|
|
568
|
-
};
|
|
569
|
-
|
|
570
|
-
(async function start() {
|
|
571
|
-
let hello = null;
|
|
572
|
-
try { hello = await fetch('/api/hello').then((r) => r.json()); } catch (e) {}
|
|
573
|
-
if (hello && hello.needsToken && !state.token) $('tokdlg').showModal();
|
|
574
|
-
let info = null;
|
|
575
|
-
try { info = await api('/api/workspaces'); } catch (e) {}
|
|
576
|
-
if (info) {
|
|
577
|
-
state.provider = info.provider; state.model = info.model;
|
|
578
|
-
setPills(info.provider, info.model);
|
|
579
|
-
$('wsname').textContent = info.cwd.split('/').pop() || info.cwd;
|
|
580
|
-
}
|
|
581
|
-
await refreshSessions();
|
|
582
|
-
setInterval(refreshSessions, 4000);
|
|
583
|
-
})();
|
|
584
|
-
`;
|
|
585
|
-
}
|
|
586
145
|
//# sourceMappingURL=ui.js.map
|