samagotchi 0.50.0 → 0.52.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +89 -1
- data/README.md +1 -1
- data/bin/chi +11 -3
- data/docs/architecture.md +1 -1
- data/docs/cli.md +40 -11
- data/docs/configuration.md +63 -21
- data/docs/hooks.md +3 -2
- data/docs/internals/engine.md +4 -2
- data/docs/memory.md +11 -0
- data/docs/plugins.md +2 -1
- data/docs/sessions.md +10 -5
- data/docs/testing.md +6 -1
- data/lib/samagotchi/bootstrap/config_writer.rb +6 -37
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
- data/lib/samagotchi/bridge.rb +29 -17
- data/lib/samagotchi/bridge_client.rb +3 -1
- data/lib/samagotchi/broadcast/recipients.rb +1 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/commands/registry.rb +2 -1
- data/lib/samagotchi/config.rb +50 -182
- data/lib/samagotchi/config_text_edit.rb +8 -32
- data/lib/samagotchi/context_sources.rb +0 -7
- data/lib/samagotchi/context_window.rb +2 -2
- data/lib/samagotchi/continue_offer.rb +5 -4
- data/lib/samagotchi/cut_policy.rb +0 -1
- data/lib/samagotchi/delivery.rb +56 -0
- data/lib/samagotchi/engine.rb +48 -17
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/host_config.rb +183 -0
- data/lib/samagotchi/host_registry.rb +21 -13
- data/lib/samagotchi/kernel_loop.rb +28 -2
- data/lib/samagotchi/last_turn.rb +48 -0
- data/lib/samagotchi/live_versions.rb +0 -6
- data/lib/samagotchi/llm/native_tool_normalizer.rb +2 -7
- data/lib/samagotchi/llm_context_override.rb +3 -22
- data/lib/samagotchi/llm_context_strategy.rb +50 -16
- data/lib/samagotchi/memory_bundle/placeholder.rb +0 -5
- data/lib/samagotchi/model_profile.rb +4 -4
- data/lib/samagotchi/model_settings.rb +46 -0
- data/lib/samagotchi/plugin/context.rb +3 -3
- data/lib/samagotchi/plugin/sessions.rb +2 -2
- data/lib/samagotchi/queued_command.rb +35 -0
- data/lib/samagotchi/reminder_store.rb +0 -27
- data/lib/samagotchi/reply_wait.rb +11 -13
- data/lib/samagotchi/sampling_settings.rb +4 -4
- data/lib/samagotchi/self_report.rb +8 -5
- data/lib/samagotchi/send_command.rb +92 -21
- data/lib/samagotchi/session.rb +45 -24
- data/lib/samagotchi/session_commands.rb +41 -3
- data/lib/samagotchi/session_inbox.rb +36 -7
- data/lib/samagotchi/session_manager.rb +21 -10
- data/lib/samagotchi/session_metrics.rb +25 -6
- data/lib/samagotchi/session_setup.rb +30 -0
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/steer_cut.rb +21 -10
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +10 -4
- data/lib/samagotchi/terminal_ui/attached_loop.rb +48 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +69 -4
- data/lib/samagotchi/terminal_ui.rb +56 -8
- data/lib/samagotchi/thinking.rb +67 -7
- data/lib/samagotchi/thinking_command.rb +72 -0
- data/lib/samagotchi/tool_activity.rb +3 -0
- data/lib/samagotchi/tool_runner.rb +2 -0
- data/lib/samagotchi/tools/builtin_calls.rb +33 -0
- data/lib/samagotchi/tools/builtins.rb +7 -2
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/task_runtime.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +4 -3
- data/lib/samagotchi/web/app.rb +59 -7
- data/lib/samagotchi/web/message_parts.rb +3 -0
- data/lib/samagotchi/web/public/activity.js +6 -0
- data/lib/samagotchi/web/public/app.js +199 -48
- data/lib/samagotchi/web/public/data.js +8 -4
- data/lib/samagotchi/web/public/format.js +8 -4
- data/lib/samagotchi/web/public/index.html +25 -20
- data/lib/samagotchi/web/public/kept_commands.js +52 -0
- data/lib/samagotchi/web/public/model_pick.js +3 -1
- data/lib/samagotchi/web/public/session_title.js +26 -0
- data/lib/samagotchi/web/public/strip.js +4 -6
- data/lib/samagotchi/web/public/thinking_chip.js +169 -0
- data/lib/samagotchi/web/public/timing.js +15 -0
- data/lib/samagotchi/web/public/turn_events.js +26 -2
- data/lib/samagotchi/web/public/turn_html.js +3 -1
- data/lib/samagotchi/web/public/turn_view.js +328 -310
- data/lib/samagotchi/web/public/vendor/lit-html.js +8 -0
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +44 -43
- data/lib/samagotchi/worker_inbound.rb +10 -7
- data/lib/samagotchi/worker_wakes.rb +0 -2
- data/lib/samagotchi/yaml_lines.rb +72 -0
- data/lib/samagotchi.rb +1 -0
- metadata +13 -1
|
@@ -45,8 +45,6 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
45
45
|
#topStrip:empty{display:grid}
|
|
46
46
|
#center{flex:1;display:flex;flex-direction:column;min-height:45vh;background:var(--bg-panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
|
47
47
|
#stage{flex:1;display:flex;flex-direction:column;position:relative;min-height:160px;background:var(--bg);overflow:hidden}
|
|
48
|
-
#firstPeek{display:none;padding:8px 12px;background:var(--accent-bg);color:var(--text);border-bottom:1px solid var(--border);font-size:13px;line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
49
|
-
#firstPeek.visible{display:block}
|
|
50
48
|
#composer #cancelBtn{order:2;background:var(--danger-bg)}
|
|
51
49
|
#history{flex:1;overflow:auto;padding:16px max(16px,calc((100% - 960px)/2));display:flex;flex-direction:column;gap:12px;background:var(--bg);min-height:160px}
|
|
52
50
|
/* No child shrinks to fit: a held (overflow:hidden) answer bubble would, clipped under the dock with nothing to scroll. */
|
|
@@ -158,6 +156,7 @@ button.task-stop{flex:none;margin-left:auto;padding:1px 8px;border:1px solid col
|
|
|
158
156
|
button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:none}
|
|
159
157
|
.ts-now button.task-stop{align-self:center}
|
|
160
158
|
.activity-params{color:var(--text-muted);overflow-wrap:anywhere}
|
|
159
|
+
.activity-alias{color:var(--text-faint);white-space:nowrap}
|
|
161
160
|
.activity-duration{color:var(--text-faint);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
162
161
|
.activity-output{margin:4px 0 2px var(--row-gutter);font-size:12px;color:var(--text-muted);white-space:pre-wrap;word-break:break-word;background:var(--bg);border-radius:6px;padding:6px 8px;max-height:160px;overflow:auto}
|
|
163
162
|
/* A row's status and tool name never wrap (a phone broke them letter by
|
|
@@ -503,8 +502,10 @@ body.has-session #composer .actions-col{flex-wrap:nowrap;flex-shrink:0}
|
|
|
503
502
|
#composer .actions-col .llmctx-new:hover{filter:none;color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
504
503
|
#composer .actions-col .llmctx-new.own{color:var(--accent);border-color:var(--accent-border)}
|
|
505
504
|
#composer .actions-col .llmctx-new[hidden],body.has-session #composer .actions-col .llmctx-new{display:none}
|
|
505
|
+
/* The new chat's thinking chip, after it: shorter text, so a smaller share of the row. */
|
|
506
|
+
#composer .actions-col .think-new{max-width:min(25%,140px)}
|
|
506
507
|
/* Phones: picker · chip · Start on one row (the key hint is gone there); names cut short rather than wrapping, the chip first. */
|
|
507
|
-
@media(max-width:600px){#composer .actions-col:has(.llmctx-new:not([hidden])){flex-wrap:nowrap}#composer .actions-col .model-pick{flex:0 1 auto;min-width:0}#composer .actions-col .llmctx-new{max-width:45%;flex-shrink:4}}
|
|
508
|
+
@media(max-width:600px){#composer .actions-col:has(.llmctx-new:not([hidden])){flex-wrap:nowrap}#composer .actions-col .model-pick{flex:0 1 auto;min-width:0}#composer .actions-col .llmctx-new{max-width:45%;flex-shrink:4}#composer .actions-col .think-new{max-width:30%}}
|
|
508
509
|
/* The model search: opaque like #commandList (a list over the conversation
|
|
509
510
|
must stay readable), above the composer, or below it (.down) when the
|
|
510
511
|
start page leaves no room above, or over the composer (.over) when
|
|
@@ -681,8 +682,6 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
681
682
|
#infoBar .meta .dir{font-family:ui-monospace,monospace;font-size:11px;color:var(--text-muted);max-width:48ch;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;text-align:left}
|
|
682
683
|
/* The footer sits on the card's glass: its chips are tinted, not solid, so the glass reads through them. */
|
|
683
684
|
#infoBar .meta .model{font-size:11px;padding:2px 6px;border-radius:6px;background:color-mix(in srgb,var(--bg-hover) 55%,transparent);color:var(--text-3)}
|
|
684
|
-
/* The memory chip ("mem 2", when the session used some): phones only, where the "mem: …" line is hidden. */
|
|
685
|
-
#infoBar .meta .mem-chip{display:none}
|
|
686
685
|
#infoBar .meta .model.served-mismatch{background:var(--warn-bg);color:var(--warn-bright);border:1px solid var(--warn-border)}
|
|
687
686
|
/* The open session's worker runs an older chi: its version, and Restart when it can. */
|
|
688
687
|
#infoBar .meta .worker-badge{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:2px 2px 2px 6px;border-radius:6px;background:var(--warn-bg);color:var(--warn-bright);border:1px solid var(--warn-border);white-space:nowrap}
|
|
@@ -690,9 +689,6 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
690
689
|
#infoBar .meta .worker-restart{padding:1px 8px;border-radius:5px;font-size:11px;font-weight:600;background:var(--bg-raised);color:var(--warn-bright);border:1px solid var(--warn-border)}
|
|
691
690
|
#infoBar .meta .worker-restart:hover:not(:disabled){background:var(--warn-border);color:var(--on-warn)}
|
|
692
691
|
#infoBar .meta .worker-restart:disabled{opacity:.7;cursor:default}
|
|
693
|
-
#infoBar .preview-line{font-size:13px;word-break:break-word;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;line-clamp:1;overflow:hidden;line-height:1.4;max-height:1.4em}
|
|
694
|
-
#infoBar .preview-line .preview, #infoBar .preview-line .mem{word-break:break-word}
|
|
695
|
-
#infoBar .mem{font-size:12px;color:var(--text-muted)}
|
|
696
692
|
#infoBar button{padding:4px 12px;border-radius:8px;font-size:12px;font-weight:600}
|
|
697
693
|
#infoBar #infoStopBtn{background:var(--bg-raised);color:var(--warn-text);border:1px solid var(--border-strong)}
|
|
698
694
|
#infoBar #infoStopBtn:hover{background:var(--warn-bg);border-color:var(--warn-border)}
|
|
@@ -700,10 +696,10 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
700
696
|
#infoBar #infoArchiveBtn:hover{color:var(--text);background:var(--bg-hover)}
|
|
701
697
|
#infoBar #infoDeleteBtn{background:transparent;color:var(--text-muted);border:1px solid transparent}
|
|
702
698
|
#infoBar #infoDeleteBtn:hover{color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
703
|
-
/* The LLM context
|
|
704
|
-
#infoBar .meta .llmctx-chip{padding:2px 6px;border-radius:6px;font-size:11px;font-weight:400;border:1px solid transparent;cursor:pointer;font-family:inherit;white-space:nowrap}
|
|
705
|
-
#infoBar .meta .llmctx-chip:hover{color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
706
|
-
#infoBar .meta .llmctx-chip.own{color:var(--accent);border-color:var(--accent-border)}
|
|
699
|
+
/* The LLM context and thinking chips (llm_context_chip.js, thinking_chip.js): model-like chips that open their popovers; accent when the session set its own. */
|
|
700
|
+
#infoBar .meta .llmctx-chip,#infoBar .meta .think-chip{padding:2px 6px;border-radius:6px;font-size:11px;font-weight:400;border:1px solid transparent;cursor:pointer;font-family:inherit;white-space:nowrap}
|
|
701
|
+
#infoBar .meta .llmctx-chip:hover,#infoBar .meta .think-chip:hover{color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
702
|
+
#infoBar .meta .llmctx-chip.own,#infoBar .meta .think-chip.own{color:var(--accent);border-color:var(--accent-border)}
|
|
707
703
|
.llmctx-form{display:flex;flex-direction:column;gap:8px;margin:0}
|
|
708
704
|
.llmctx-form .ctx-pop-head strong{font-family:inherit}
|
|
709
705
|
.llmctx-field{display:flex;align-items:center;gap:8px}
|
|
@@ -744,12 +740,11 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
744
740
|
.ctx-pop-actions button:hover{color:var(--text-hover)}
|
|
745
741
|
.ctx-pop-actions .ctx-pop-detach{color:var(--text-muted);background:transparent}
|
|
746
742
|
.ctx-pop-actions .ctx-pop-detach:hover{color:var(--danger-text);border-color:color-mix(in srgb,var(--danger) 55%,transparent)}
|
|
747
|
-
/* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable)
|
|
743
|
+
/* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable). */
|
|
748
744
|
@media(max-width:600px){
|
|
749
745
|
/* The key hint: cut off at this width, and phones have no Shift+Enter. */
|
|
750
746
|
#composeHint{display:none}
|
|
751
747
|
#infoBar{flex-wrap:nowrap;align-items:center;gap:6px;padding:6px 10px 8px}
|
|
752
|
-
#infoBar .preview-line{display:none}
|
|
753
748
|
/* No terminal on a phone to attach from. */
|
|
754
749
|
#infoBar .meta .copy-attach{display:none}
|
|
755
750
|
#infoBar .meta{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;white-space:nowrap}
|
|
@@ -757,8 +752,8 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
757
752
|
#infoBar .meta>*{flex-shrink:0}
|
|
758
753
|
#infoBar .meta .id{order:-2}
|
|
759
754
|
#infoBar .meta .status{order:-1}
|
|
760
|
-
/*
|
|
761
|
-
#infoBar .meta .mem-chip{
|
|
755
|
+
/* The memory chip right after the status (same order, later in the DOM): the meta scrolls, and only its start is in sight. */
|
|
756
|
+
#infoBar .meta .mem-chip{order:-1}
|
|
762
757
|
/* A parent's children chip too: its waiting part is what to see. */
|
|
763
758
|
#infoBar .meta .children-chip{order:-1}
|
|
764
759
|
/* A chain's ← → links too: the way to the other days. */
|
|
@@ -825,6 +820,14 @@ body.has-session #scope .scope-where{display:none}
|
|
|
825
820
|
/* The ▾ glyph draws tiny at the pill's 11 px: at 20 px it is about as tall
|
|
826
821
|
as the pill text's capitals (phone and desktop alike). */
|
|
827
822
|
/* The bell (notifications): quiet at the top bar's right end; filled in the accent when on, struck through when the browser blocks it. */
|
|
823
|
+
/* The open session's title, when no strip card in sight shows it (session_title.js): the bar's middle, cut with an ellipsis; the whole first message in its tooltip. */
|
|
824
|
+
#sessionTitle{flex:1;min-width:0;font-size:13px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
825
|
+
#sessionTitle[hidden]{display:none}
|
|
826
|
+
/* A phone: the title is its own row under the bar, so "chi + · pill · bell" stay one line. */
|
|
827
|
+
@media(max-width:600px){
|
|
828
|
+
#topBar{flex-wrap:wrap;row-gap:2px}
|
|
829
|
+
#sessionTitle{flex-basis:100%;order:10;font-size:12px}
|
|
830
|
+
}
|
|
828
831
|
#notifyBtn{margin-left:auto;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;min-height:0;background:transparent;border:1px solid transparent;border-radius:999px;color:var(--text-muted)}
|
|
829
832
|
#notifyBtn[hidden]{display:none}
|
|
830
833
|
#notifyBtn:hover{color:var(--text-2);border-color:var(--border)}
|
|
@@ -947,7 +950,8 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
947
950
|
.card.archived{opacity:.6}
|
|
948
951
|
.card.archived:hover{opacity:.85}
|
|
949
952
|
.card .archived-badge,.family-row .archived-badge{font-size:10px;padding:1px 6px;border-radius:6px;border:1px solid var(--border-strong);color:var(--text-muted)}
|
|
950
|
-
|
|
953
|
+
/* bottom: above the session's composer card when one shows (--toast-bottom, app.js placeToast). */
|
|
954
|
+
#toast{position:fixed;left:50%;bottom:var(--toast-bottom,24px);transform:translateX(-50%);width:max-content;max-width:calc(100vw - 32px);box-sizing:border-box;z-index:50;display:flex;gap:12px;align-items:center;padding:8px 14px;border-radius:10px;background:var(--bg-raised);color:var(--text);border:1px solid var(--border-strong);box-shadow:var(--toast-shadow);font-size:13px}
|
|
951
955
|
#toast[hidden]{display:none}
|
|
952
956
|
#toast .toast-action{background:transparent;border:0;color:var(--accent);font-weight:600;cursor:pointer;padding:2px 4px}
|
|
953
957
|
#toast .toast-close{background:transparent;border:0;color:var(--text-muted);cursor:pointer;padding:2px 4px;margin-left:-6px}
|
|
@@ -1016,11 +1020,11 @@ a{color:var(--accent)}
|
|
|
1016
1020
|
<div id="scope" hidden></div>
|
|
1017
1021
|
<button type="button" class="strip-toggle" id="stripShow" title="Show the latest sessions"><span class="pill-arrow" aria-hidden="true">▾</span> sessions</button>
|
|
1018
1022
|
<button type="button" class="strip-toggle" id="stripHideTop" title="Hide the latest sessions"><span class="pill-arrow" aria-hidden="true">▴</span> hide</button>
|
|
1023
|
+
<span id="sessionTitle" hidden></span>
|
|
1019
1024
|
<button type="button" id="notifyBtn" aria-label="Notifications" aria-pressed="false" hidden><svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true"><path class="bell" d="M8 1.75a4 4 0 0 0-4 4v2.6L2.75 10.6v.9h10.5v-.9L12 8.35v-2.6a4 4 0 0 0-4-4Z"/><path class="clapper" d="M6.5 13a1.5 1.5 0 0 0 3 0"/><path class="slash" d="M2.5 2.5l11 11"/></svg></button>
|
|
1020
1025
|
</div>
|
|
1021
1026
|
<section id="topStrip" aria-label="Latest sessions" class="loading"><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div></section>
|
|
1022
1027
|
<section id="center">
|
|
1023
|
-
<div id="firstPeek"></div>
|
|
1024
1028
|
<div id="stage">
|
|
1025
1029
|
<div id="markdownWarning" class="markdown-warning" hidden></div>
|
|
1026
1030
|
<div id="history"></div>
|
|
@@ -1044,8 +1048,9 @@ a{color:var(--accent)}
|
|
|
1044
1048
|
<div class="actions-col">
|
|
1045
1049
|
<button type="button" id="modelPick" class="model-pick" title="The model the new chat starts on" aria-label="Model for the new chat" aria-haspopup="listbox" aria-expanded="false" hidden><span class="model-pick-name"></span></button>
|
|
1046
1050
|
<button type="button" id="llmContextNew" class="llmctx-new" aria-haspopup="dialog" hidden></button>
|
|
1051
|
+
<button type="button" id="thinkingNew" class="llmctx-new think-new" aria-haspopup="dialog" hidden></button>
|
|
1047
1052
|
<div id="composeHint">Enter to start · Shift+Enter for a newline</div>
|
|
1048
|
-
<button id="cancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">
|
|
1053
|
+
<button id="cancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">Stop turn</button>
|
|
1049
1054
|
<div class="split">
|
|
1050
1055
|
<button id="actionBtn">Start</button>
|
|
1051
1056
|
</div>
|
|
@@ -1056,7 +1061,7 @@ a{color:var(--accent)}
|
|
|
1056
1061
|
<div id="contextChips" aria-label="Attached context" hidden></div>
|
|
1057
1062
|
<button class="ghost" id="infoContinueBtn" title="Continue: start the next link of this session's chain from its recap (this one is archived)" style="display:none">continue →</button>
|
|
1058
1063
|
<button class="ghost" id="infoArchiveBtn" title="Archive: hide from the lists, keep for good" style="display:none">archive</button>
|
|
1059
|
-
<button class="ghost" id="infoStopBtn" title="Stop session (terminate worker)" style="display:none">stop</button>
|
|
1064
|
+
<button class="ghost" id="infoStopBtn" title="Stop session (terminate worker)" style="display:none">stop session</button>
|
|
1060
1065
|
<button class="ghost" id="infoDeleteBtn" title="Delete session (for good)" style="display:none">delete</button>
|
|
1061
1066
|
</div>
|
|
1062
1067
|
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Command bubbles (/help, /model, /stats, /llm-context …) live only in the
|
|
2
|
+
// page: the session saves just a `!cmd`'s output (a message, redrawn as its
|
|
3
|
+
// command bubble). A resync (a `!cmd`, a `!rollback`, a dropped stream)
|
|
4
|
+
// redraws the history from the server, so the page keeps its own command
|
|
5
|
+
// bubbles across it: each one's anchor is the prompts and saved shell
|
|
6
|
+
// commands before it, and it goes back after them in the redrawn history.
|
|
7
|
+
// A reload still drops them.
|
|
8
|
+
//
|
|
9
|
+
// +kinds+: the history's children in order, each "user" (a prompt bubble),
|
|
10
|
+
// "shell" (a `!cmd`'s bubble, which the redrawn history has too) or
|
|
11
|
+
// anything else.
|
|
12
|
+
|
|
13
|
+
// @return {{users: number, shells: number}} the anchor of kinds[index]
|
|
14
|
+
export function commandAnchor(kinds, index) {
|
|
15
|
+
const before = kinds.slice(0, index);
|
|
16
|
+
return {
|
|
17
|
+
users: before.filter((k) => k === "user").length,
|
|
18
|
+
shells: before.filter((k) => k === "shell").length,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The index to insert a kept bubble at: before the first prompt or shell
|
|
23
|
+
// command past its anchor (it ran after its turn), else at the end (a
|
|
24
|
+
// history a !rollback made shorter too).
|
|
25
|
+
export function anchorInsertIndex(kinds, { users = 0, shells = 0 } = {}) {
|
|
26
|
+
let seenUsers = 0;
|
|
27
|
+
let seenShells = 0;
|
|
28
|
+
for (let i = 0; i < kinds.length; i += 1) {
|
|
29
|
+
if (kinds[i] === "user") {
|
|
30
|
+
if (seenUsers >= users) return i;
|
|
31
|
+
seenUsers += 1;
|
|
32
|
+
} else if (kinds[i] === "shell") {
|
|
33
|
+
if (seenShells >= shells) return i;
|
|
34
|
+
seenShells += 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return kinds.length;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SHELL_LINE = /^!\s*\S/;
|
|
41
|
+
const ROLLBACK_LINE = /^!rollback(\s|$)/;
|
|
42
|
+
|
|
43
|
+
// Whether a command bubble's line is kept across a resync: a `!cmd`'s is
|
|
44
|
+
// redrawn from its saved message (keeping it too would show it twice).
|
|
45
|
+
export function isShellCommandLine(line) {
|
|
46
|
+
const text = String(line ?? "");
|
|
47
|
+
return SHELL_LINE.test(text) && !ROLLBACK_LINE.test(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function keepsCommandBubble(line) {
|
|
51
|
+
return !isShellCommandLine(line);
|
|
52
|
+
}
|
|
@@ -22,7 +22,8 @@ export const RECENT_KEY = "chi_model_recent";
|
|
|
22
22
|
|
|
23
23
|
// The payload's models as rows: {name, host, id, isDefaultHost}, and
|
|
24
24
|
// sampling (the configured request parameters as /model words them) when
|
|
25
|
-
// the model has some,
|
|
25
|
+
// the model has some, thinking (the level it starts at, Thinking::Explained#summary),
|
|
26
|
+
// and llm_context (the LLM context it starts under,
|
|
26
27
|
// LLMContextStrategy::Explained#summary) when the server sent one, and
|
|
27
28
|
// configured when the host declares the id (hosts.<name>.models), and
|
|
28
29
|
// unavailable when that host failed to list its models. The
|
|
@@ -42,6 +43,7 @@ export function modelRows(payload) {
|
|
|
42
43
|
if (m.configured === true) row.configured = true;
|
|
43
44
|
if (m.unavailable === true) row.unavailable = true;
|
|
44
45
|
if (m.llm_context && typeof m.llm_context === "object") row.llm_context = m.llm_context;
|
|
46
|
+
if (m.thinking && typeof m.thinking === "object") row.thinking = m.thinking;
|
|
45
47
|
return row;
|
|
46
48
|
});
|
|
47
49
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { normalize } from "./format.js";
|
|
2
|
+
|
|
3
|
+
// Where the open session's title shows: once on screen. Its strip card says
|
|
4
|
+
// it when the strip is shown and has an `.active` card for it; otherwise
|
|
5
|
+
// (strip hidden, an older session without a card, a delegate folded into its
|
|
6
|
+
// parent's card) the top bar does. The All sessions view shows none.
|
|
7
|
+
// Plain rules, no DOM; app.js wires them (updateSessionTitle).
|
|
8
|
+
|
|
9
|
+
const TAB_TITLE_CHARS = 40;
|
|
10
|
+
|
|
11
|
+
// The top bar's text, or "" when it shows no title.
|
|
12
|
+
export function topBarTitle({ title, cardShown, stripHidden, allView }) {
|
|
13
|
+
const text = normalize(title);
|
|
14
|
+
if (!text || allView) return "";
|
|
15
|
+
if (cardShown && !stripHidden) return "";
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The browser tab: "<title, cut at 40> · <base>", the base alone without a
|
|
20
|
+
// session title (base = "Chi · <project>" or "Chi").
|
|
21
|
+
export function tabTitle({ title, base }) {
|
|
22
|
+
const text = normalize(title);
|
|
23
|
+
if (!text) return base;
|
|
24
|
+
const cut = text.length > TAB_TITLE_CHARS ? `${text.slice(0, TAB_TITLE_CHARS)}…` : text;
|
|
25
|
+
return `${cut} · ${base}`;
|
|
26
|
+
}
|
|
@@ -18,13 +18,11 @@ export function stripColumns(width) {
|
|
|
18
18
|
// And how many wait on the user (notify.js waitingOn: a question, an
|
|
19
19
|
// approval, a card), so a hidden strip still says one needs an answer.
|
|
20
20
|
// Plain text parts; live and waiting are "" when none. Zero sessions keep
|
|
21
|
-
// the bare label.
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
// session, a waiting delegate is still one thing waiting on the user
|
|
25
|
-
export function stripShowParts(sessions, { cards = null } = {}) {
|
|
21
|
+
// the bare label. The count is sessions (a folded delegate counts), the
|
|
22
|
+
// number the "All sessions" tile shows.
|
|
23
|
+
export function stripShowParts(sessions) {
|
|
26
24
|
const list = sessions || [];
|
|
27
|
-
const n =
|
|
25
|
+
const n = list.length;
|
|
28
26
|
const liveN = list.filter((s) => s && s.owner === "worker").length;
|
|
29
27
|
const waitingN = list.filter((s) => waitingOn(s)).length;
|
|
30
28
|
return {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// The session bar's thinking chip: the level the next turn runs at ("think
|
|
2
|
+
// low"; "· session" when the session set its own), and a popover with a
|
|
3
|
+
// level select that runs /thinking in the session's worker (the command's
|
|
4
|
+
// reply shows in the conversation, like a typed one). The level and where
|
|
5
|
+
// it came from are the session's thinking (Thinking::Explained#summary:
|
|
6
|
+
// {level, source, own}; the worker's snapshot, a command_ran, or the
|
|
7
|
+
// server's own reading of the session file).
|
|
8
|
+
//
|
|
9
|
+
// The start page has one too (kind "new chat"): it shows the picked
|
|
10
|
+
// model's level (GET /api/models' thinking) and keeps a choice for the
|
|
11
|
+
// next create only (POST /api/sessions' thinking), never remembered:
|
|
12
|
+
// nothing is sent unless the user sets one.
|
|
13
|
+
|
|
14
|
+
import { NEW_CHAT, SESSION, popoverPlace } from "./llm_context_chip.js";
|
|
15
|
+
|
|
16
|
+
export const LEVELS = ["off", "low", "medium", "high"];
|
|
17
|
+
const DEFAULT = "default";
|
|
18
|
+
|
|
19
|
+
// The new chat's choice (a level, or null) laid over the model's summary:
|
|
20
|
+
// what the chip shows. The model's own summary without a choice.
|
|
21
|
+
export function withChoice(summary, choice) {
|
|
22
|
+
if (!summary || !choice) return summary;
|
|
23
|
+
return { ...summary, level: choice, source: SESSION, own: choice };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The chip: its text, tooltip and whether the session (or the new chat)
|
|
27
|
+
// set its own; null without a summary (an older worker). The default with
|
|
28
|
+
// nothing set anywhere shows too ("think default"): the chip is where the
|
|
29
|
+
// level is set.
|
|
30
|
+
export function chipModel(summary, { kind = SESSION } = {}) {
|
|
31
|
+
if (!summary || !summary.level) return null;
|
|
32
|
+
const own = !!summary.own;
|
|
33
|
+
const what = kind === NEW_CHAT ? "the new chat" : "this session";
|
|
34
|
+
const from = own ? what : summary.source || "the model's own";
|
|
35
|
+
const title = `Thinking: ${summary.level} (${from}). Click to change it for ${what}.`;
|
|
36
|
+
return { text: `think ${summary.level}${own ? ` · ${kind}` : ""}`, title, own };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function chipHtml(summary, escapeHtml) {
|
|
40
|
+
const chip = chipModel(summary);
|
|
41
|
+
if (!chip) return "";
|
|
42
|
+
return `<button type="button" class="model think-chip${chip.own ? " own" : ""}" title="${escapeHtml(chip.title)}" ` +
|
|
43
|
+
`data-think-chip aria-haspopup="dialog">${escapeHtml(chip.text)}</button>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The /thinking line for a select value ("default" unsets the session's
|
|
47
|
+
// own); null when it is what the form opened with.
|
|
48
|
+
export function commandLine(level, initial = null) {
|
|
49
|
+
if (initial !== null && level === initial) return null;
|
|
50
|
+
return `/thinking ${level}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function option(value, label, chosen, escapeHtml) {
|
|
54
|
+
return `<option value="${escapeHtml(value)}"${value === chosen ? " selected" : ""}>${escapeHtml(label)}</option>`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// "Now: low (session)", "Now: default (the model's own)".
|
|
58
|
+
export function nowText(summary) {
|
|
59
|
+
return `Now: ${summary.level} (${summary.source || "the model's own"})`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The popover's form. For the new chat (kind NEW_CHAT), +summary+ is the
|
|
63
|
+
// model's and +choice+ the level the form shows.
|
|
64
|
+
export function formHtml(summary, escapeHtml, { running = false, kind = SESSION, choice = null } = {}) {
|
|
65
|
+
const fresh = kind === NEW_CHAT;
|
|
66
|
+
const chosen = (fresh ? choice : summary.own) || DEFAULT;
|
|
67
|
+
const follow = summary.own && !fresh ? "follow the model" : `follow the model (${summary.level})`;
|
|
68
|
+
const when = fresh ? "For the new chat's first turn on; not remembered for the next one."
|
|
69
|
+
: "From the next turn's start. On Gemma the whole prompt is read again; a hosted API may restart its cache.";
|
|
70
|
+
return `<form class="think-form llmctx-form"><div class="ctx-pop-head"><strong>Thinking</strong>` +
|
|
71
|
+
`<span class="ctx-pop-kind">${fresh ? NEW_CHAT : "this session"}</span>` +
|
|
72
|
+
`<button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div>` +
|
|
73
|
+
`<div class="ctx-pop-meta think-now">${escapeHtml(nowText(summary))}</div>` +
|
|
74
|
+
`<label class="llmctx-field"><span>Level</span><select name="level">` +
|
|
75
|
+
option(DEFAULT, follow, chosen, escapeHtml) +
|
|
76
|
+
LEVELS.map((level) => option(level, level, chosen, escapeHtml)).join("") + `</select></label>` +
|
|
77
|
+
`<div class="ctx-pop-meta">${when}${running ? " A turn is running: it applies after this turn." : ""}</div>` +
|
|
78
|
+
`<div class="ctx-pop-actions"><button type="submit" class="llmctx-set">Set</button></div></form>`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The chip's popover, as createLlmContextControl's: el is where the chip
|
|
82
|
+
// is drawn (clicks are delegated: #infoText is redrawn every tick);
|
|
83
|
+
// summary(): the open session's thinking; session(): its id; running():
|
|
84
|
+
// a turn runs; run(line): sends the command. The start page's (kind
|
|
85
|
+
// NEW_CHAT): summary() is the picked model's, choice() the pending level
|
|
86
|
+
// and choose(next) takes a Set's (null for "follow the model"). id and
|
|
87
|
+
// anchor (the chip's selector) keep it apart from the llm ctx control on
|
|
88
|
+
// the same element.
|
|
89
|
+
export function createThinkingControl({ el, escapeHtml, summary, session = () => null, running = () => false, run,
|
|
90
|
+
kind = SESSION, choice = () => null, choose = null,
|
|
91
|
+
id = "thinkingPopover", anchor: anchorSel = "[data-think-chip]",
|
|
92
|
+
doc = document, win = window }) {
|
|
93
|
+
const fresh = kind === NEW_CHAT;
|
|
94
|
+
const pop = doc.createElement("div");
|
|
95
|
+
pop.id = id;
|
|
96
|
+
pop.className = "ctx-popover think-popover";
|
|
97
|
+
pop.setAttribute("role", "dialog");
|
|
98
|
+
pop.hidden = true;
|
|
99
|
+
doc.body.appendChild(pop);
|
|
100
|
+
let initial = null;
|
|
101
|
+
let openFor = null;
|
|
102
|
+
|
|
103
|
+
function place(anchor) {
|
|
104
|
+
const width = Math.min(320, win.innerWidth - 32);
|
|
105
|
+
pop.style.width = `${width}px`;
|
|
106
|
+
pop.style.maxHeight = "";
|
|
107
|
+
const at = popoverPlace(anchor.getBoundingClientRect(), { width, height: pop.offsetHeight,
|
|
108
|
+
innerWidth: win.innerWidth, innerHeight: win.innerHeight });
|
|
109
|
+
pop.style.left = `${at.left}px`;
|
|
110
|
+
pop.style.top = at.top === undefined ? "" : `${at.top}px`;
|
|
111
|
+
pop.style.bottom = at.bottom === undefined ? "" : `${at.bottom}px`;
|
|
112
|
+
if (at.maxHeight !== undefined) pop.style.maxHeight = `${at.maxHeight}px`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function open(anchor) {
|
|
116
|
+
const data = summary();
|
|
117
|
+
if (!data) return;
|
|
118
|
+
initial = (fresh ? choice() : data.own) || DEFAULT;
|
|
119
|
+
openFor = session();
|
|
120
|
+
pop.innerHTML = formHtml(data, escapeHtml, { running: running(), kind, choice: choice() });
|
|
121
|
+
pop.hidden = false;
|
|
122
|
+
place(anchor);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function close() {
|
|
126
|
+
pop.hidden = true;
|
|
127
|
+
initial = null;
|
|
128
|
+
openFor = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isOpen() { return !pop.hidden; }
|
|
132
|
+
|
|
133
|
+
// A new summary: an open form keeps the user's pick; only "Now:" follows.
|
|
134
|
+
function refresh() {
|
|
135
|
+
if (!isOpen()) return;
|
|
136
|
+
const data = summary();
|
|
137
|
+
if (!data || session() !== openFor) return close();
|
|
138
|
+
const now = pop.querySelector(".think-now");
|
|
139
|
+
if (now) now.textContent = nowText(data);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
el.addEventListener("click", (e) => {
|
|
143
|
+
const anchor = e.target.closest(anchorSel);
|
|
144
|
+
if (!anchor) return;
|
|
145
|
+
e.stopPropagation();
|
|
146
|
+
if (isOpen()) close();
|
|
147
|
+
else open(anchor);
|
|
148
|
+
});
|
|
149
|
+
pop.addEventListener("submit", (e) => {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
const level = e.target.elements.level.value;
|
|
152
|
+
const was = initial;
|
|
153
|
+
close();
|
|
154
|
+
if (fresh) return choose(level === DEFAULT ? null : level);
|
|
155
|
+
const line = commandLine(level, was);
|
|
156
|
+
if (line) run(line);
|
|
157
|
+
});
|
|
158
|
+
pop.addEventListener("click", (e) => {
|
|
159
|
+
if (e.target.closest("[data-act]")?.dataset.act === "close") close();
|
|
160
|
+
});
|
|
161
|
+
doc.addEventListener("click", (e) => {
|
|
162
|
+
if (isOpen() && !pop.contains(e.target) && !e.target.closest?.(anchorSel)) close();
|
|
163
|
+
});
|
|
164
|
+
doc.addEventListener("keydown", (e) => {
|
|
165
|
+
if (e.key === "Escape" && isOpen()) close();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return { close, refresh, get open() { return isOpen(); } };
|
|
169
|
+
}
|
|
@@ -63,6 +63,18 @@ export function elapsedSince(startedAt, now = Date.now()) {
|
|
|
63
63
|
return Math.max(0, current - started);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// The info bar's session clock: while a turn runs it counts from the
|
|
67
|
+
// session's start (+timing.startedAt+), or, on a session's first turn (its
|
|
68
|
+
// snapshot came before any turn, so no start), from the running turn's;
|
|
69
|
+
// otherwise the server's session_duration_ms (null when unknown).
|
|
70
|
+
export function sessionDurationNow(timing, running, now = Date.now()) {
|
|
71
|
+
if (running) {
|
|
72
|
+
const since = elapsedSince(timing?.startedAt ?? timing?.activeTurn?.started_at, now);
|
|
73
|
+
if (since !== null) return since;
|
|
74
|
+
}
|
|
75
|
+
return timing?.sessionDurationMs ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
export function normalizeTiming(data = {}) {
|
|
67
79
|
return {
|
|
68
80
|
startedAt: typeof data.started_at === "string" ? data.started_at : null,
|
|
@@ -251,9 +263,12 @@ function canceledSteps(g, record, tools) {
|
|
|
251
263
|
return record?.status === "canceled" && (g.texts.length > 1 || tools.length > 0);
|
|
252
264
|
}
|
|
253
265
|
|
|
266
|
+
// A call's row from its tool record alone (no saved parts): the record's
|
|
267
|
+
// called_as too, so "(called as bash)" shows after a reload, as live.
|
|
254
268
|
function recordRows(tools, iteration) {
|
|
255
269
|
return tools.filter((r) => Number(r.iteration) === iteration).map((r) => ({
|
|
256
270
|
key: `${iteration}:${r.call_index ?? 0}`, tool: r.tool, status: r.status, duration_ms: r.duration_ms,
|
|
271
|
+
...(r.called_as ? { called_as: r.called_as } : {}),
|
|
257
272
|
}));
|
|
258
273
|
}
|
|
259
274
|
|
|
@@ -244,6 +244,29 @@ export function sessionCommandLine(text, commands) {
|
|
|
244
244
|
.some((c) => c && typeof c.name === "string" && (line === c.name || line.startsWith(`${c.name} `)));
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// /cut TEXT and /queue TEXT: the UIs' own words for a delivery (listed
|
|
248
|
+
// for Tab and /help, never run as commands; TerminalUI::Formatting's
|
|
249
|
+
// STEER_CHOICE: the word, then any whitespace and the text). During a
|
|
250
|
+
// turn /cut TEXT is the text sent as a cut (POST /turn, delivery "cut"),
|
|
251
|
+
// as chi send --cut does; /queue isn't in the web yet (the page says so,
|
|
252
|
+
// as the attached TUI does). With no running turn both are the text as a
|
|
253
|
+
// normal turn. The bare word gets the usage line.
|
|
254
|
+
// @return {{send: string, delivery?: string} | {reply: string} | null}
|
|
255
|
+
// null: not a delivery line
|
|
256
|
+
export const STEER_CHOICE_USAGE = "(/cut and /queue need a message: /cut TEXT, /queue TEXT)";
|
|
257
|
+
export const QUEUE_UNAVAILABLE_WEB = "(/queue isn't available in the web yet; a plain message goes in at the running turn's next step, and /cut TEXT cuts in)";
|
|
258
|
+
const STEER_CHOICE = /^\/(cut|queue)(?:\s+([\s\S]*))?$/;
|
|
259
|
+
|
|
260
|
+
export function steerChoiceRoute(text, { running = false } = {}) {
|
|
261
|
+
const match = STEER_CHOICE.exec(String(text || "").trim());
|
|
262
|
+
if (!match) return null;
|
|
263
|
+
const message = (match[2] || "").trim();
|
|
264
|
+
if (!message) return { reply: STEER_CHOICE_USAGE };
|
|
265
|
+
if (!running) return { send: message };
|
|
266
|
+
if (match[1] === "queue") return { reply: QUEUE_UNAVAILABLE_WEB };
|
|
267
|
+
return { send: message, delivery: "cut" };
|
|
268
|
+
}
|
|
269
|
+
|
|
247
270
|
// A line that is one word: a slash and a name, no spaces, no second slash
|
|
248
271
|
// (Commands::Registry#unknown_command_word?). `/modle` is one; `/foo bar`
|
|
249
272
|
// and `/usr/bin/env` are not (they are prompts).
|
|
@@ -558,8 +581,9 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
558
581
|
const call = { iteration: part.iteration, call_index: part.call_index, tool: part.tool };
|
|
559
582
|
if (part.label) call.label = part.label;
|
|
560
583
|
const title = part.title ? { title: part.title } : {};
|
|
584
|
+
const alias = part.called_as ? { called_as: part.called_as } : {};
|
|
561
585
|
const view = part.view ? { view: part.view } : {};
|
|
562
|
-
events.push({ type: "tool_call_started", ...call, params: part.params, ...title, ...view });
|
|
586
|
+
events.push({ type: "tool_call_started", ...call, params: part.params, ...title, ...alias, ...view });
|
|
563
587
|
if (part.status !== "running") {
|
|
564
588
|
// The activity as the live event had it; action and duration_ms
|
|
565
589
|
// only in snapshots from a worker that keeps them (the TUI's
|
|
@@ -567,7 +591,7 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
567
591
|
const action = part.action ? { action: part.action } : {};
|
|
568
592
|
const completed = {
|
|
569
593
|
type: "tool_call_completed", ...call, output: part.output, output_truncated: !!part.output_truncated,
|
|
570
|
-
activity: { ...action, tool: part.tool, status: part.status, params: part.params, ...title },
|
|
594
|
+
activity: { ...action, tool: part.tool, status: part.status, params: part.params, ...title, ...alias },
|
|
571
595
|
};
|
|
572
596
|
if (part.duration_ms != null) completed.duration_ms = part.duration_ms;
|
|
573
597
|
if (part.diff) completed.diff = part.diff;
|
|
@@ -78,13 +78,15 @@ export function toolRowInnerHtml(row, { thumbs = () => "" } = {}) {
|
|
|
78
78
|
const shown = row.title || row.params;
|
|
79
79
|
const hover = rowHover(row) ? ` title="${escapeHtml(rowHover(row))}"` : "";
|
|
80
80
|
const params = shown ? `<span class="activity-params"${hover}>${escapeHtml(shown)}</span>` : "";
|
|
81
|
+
// The name the model called the tool by when it was an alias's, dim.
|
|
82
|
+
const alias = row.called_as ? `<span class="activity-alias">(called as ${escapeHtml(row.called_as)})</span>` : "";
|
|
81
83
|
const duration = formatDuration(row.duration_ms);
|
|
82
84
|
const full = row.status === "running" ? "" : String(row.output || "").replace(/^\[[^\]]*\]\s*/, "");
|
|
83
85
|
const output = full
|
|
84
86
|
? `<div class="activity-output" title="${escapeHtml(full)}">${escapeHtml(full.length > 300 ? `${full.slice(0, 300)}…` : full)}</div>`
|
|
85
87
|
: "";
|
|
86
88
|
const status = statusParts(row.status);
|
|
87
|
-
return `${statusMarkHtml(status)}<span class="activity-tool">${escapeHtml(toolName(row))}</span>${params}${statusWordHtml(status)}` +
|
|
89
|
+
return `${statusMarkHtml(status)}<span class="activity-tool">${escapeHtml(toolName(row))}</span>${params}${alias}${statusWordHtml(status)}` +
|
|
88
90
|
`${duration ? `<span class="activity-duration">${escapeHtml(duration)}</span>` : ""}` +
|
|
89
91
|
`${commandBlockHtml(row.view, row.title)}${llmEditHtml(row)}${output}` +
|
|
90
92
|
`${row.images?.length ? thumbs(row.images) : ""}${row.diff ? activityDiffHtml(row.diff) : ""}`;
|