samagotchi 0.148.0 → 0.150.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 +62 -1
- data/README.md +4 -2
- data/docs/architecture.md +3 -3
- data/docs/bundles.md +38 -1
- data/docs/cli.md +76 -24
- data/docs/configuration.md +8 -6
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +2 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +8 -0
- data/docs/internals/bundles.md +42 -14
- data/docs/internals/objectives.md +269 -0
- data/docs/internals/web-frontend.md +4 -1
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +47 -9
- data/docs/plugins.md +10 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +45 -2
- data/docs/system-one.md +17 -1
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_store.rb +52 -7
- data/lib/samagotchi/bridge.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +6 -4
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +9 -0
- data/lib/samagotchi/bundles/decision-card/manifest.yml +13 -0
- data/lib/samagotchi/bundles/decision-card/plugin.rb +280 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/chi_home.rb +4 -2
- data/lib/samagotchi/chi_shim.rb +2 -1
- data/lib/samagotchi/child_env.rb +10 -3
- data/lib/samagotchi/config.rb +7 -1
- data/lib/samagotchi/context_note.rb +17 -5
- data/lib/samagotchi/engine.rb +113 -6
- data/lib/samagotchi/engine_peers.rb +11 -0
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/pair_boundary.rb +3 -2
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/llm/chat_loop.rb +1 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +16 -2
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +62 -1
- data/lib/samagotchi/memory_bundle/installer.rb +161 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/merger.rb +209 -8
- data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
- data/lib/samagotchi/memory_bundle/profile.rb +7 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +178 -20
- data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
- data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
- data/lib/samagotchi/memory_bundle/source.rb +3 -3
- data/lib/samagotchi/memory_bundle/status.rb +21 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
- data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
- data/lib/samagotchi/memory_bundle/trash.rb +58 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +6 -2
- data/lib/samagotchi/objectives.rb +395 -0
- data/lib/samagotchi/objectives_audit.rb +115 -0
- data/lib/samagotchi/objectives_check.rb +365 -0
- data/lib/samagotchi/objectives_command.rb +192 -0
- data/lib/samagotchi/plugin/api.rb +2 -1
- data/lib/samagotchi/plugin/context.rb +21 -1
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/session_commands.rb +15 -0
- data/lib/samagotchi/session_continue.rb +3 -0
- data/lib/samagotchi/session_inbox.rb +14 -6
- data/lib/samagotchi/session_manager.rb +1 -1
- data/lib/samagotchi/steer.rb +24 -4
- data/lib/samagotchi/testing/boundary.rb +2 -0
- data/lib/samagotchi/testing/host.rb +1 -0
- data/lib/samagotchi/testing/minitest.rb +3 -0
- data/lib/samagotchi/testing/rspec.rb +3 -0
- data/lib/samagotchi/testing/sandbox.rb +3 -1
- data/lib/samagotchi/tool_activity.rb +17 -0
- data/lib/samagotchi/tool_declarations.rb +34 -0
- data/lib/samagotchi/tools/builtin_calls.rb +2 -1
- data/lib/samagotchi/tools/builtins.rb +4 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/objectives_tool.rb +75 -0
- data/lib/samagotchi/tools/peers.rb +3 -0
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_flow.rb +3 -1
- data/lib/samagotchi/update_command.rb +76 -28
- data/lib/samagotchi/update_hint.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/message_display.rb +9 -0
- data/lib/samagotchi/web/participant_view.rb +2 -2
- data/lib/samagotchi/web/public/app.js +15 -3
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/bubbles.js +4 -2
- data/lib/samagotchi/web/public/cards_place.js +30 -9
- data/lib/samagotchi/web/public/commands.js +4 -1
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/index.html +20 -14
- data/lib/samagotchi/web/public/objectives.css +80 -0
- data/lib/samagotchi/web/public/objectives_entry.js +22 -0
- data/lib/samagotchi/web/public/objectives_model.js +100 -0
- data/lib/samagotchi/web/public/objectives_moves.js +145 -0
- data/lib/samagotchi/web/public/objectives_strip.js +242 -0
- data/lib/samagotchi/web/public/objectives_wiring.js +62 -0
- data/lib/samagotchi/web/public/stage_model.js +5 -6
- data/lib/samagotchi/web/public/stage_view.js +8 -7
- data/lib/samagotchi/web/public/stream_handlers.js +6 -1
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/public/update_sheet.js +8 -3
- data/lib/samagotchi/web/public/working_set_view.js +10 -7
- data/lib/samagotchi/web/push/notifier.rb +7 -9
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_show.rb +12 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- data/lib/samagotchi/worker.rb +3 -0
- data/lib/samagotchi/worker_inbound.rb +16 -8
- metadata +22 -1
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
<link rel="stylesheet" href="/assets/annotate_bar.css">
|
|
15
15
|
<link rel="stylesheet" href="/assets/composer.css">
|
|
16
16
|
<link rel="stylesheet" href="/assets/forms.css">
|
|
17
|
+
<link rel="stylesheet" href="/assets/objectives.css">
|
|
17
18
|
<style>
|
|
18
19
|
/* Icon masks shared by the phone info bar and the all view's cards (the stroke only shapes the mask; the icon takes currentColor); the image icon is composer.css's. */
|
|
19
20
|
:root{--icon-archive:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='3.5' rx='1'/%3E%3Cpath d='M3.25 6.25v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1v-6M6.5 9h3'/%3E%3C/svg%3E");--icon-unarchive:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='3.5' rx='1'/%3E%3Cpath d='M3.25 6.25v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1v-6M8 11.5V8.25M6.5 9.75 8 8.25l1.5 1.5'/%3E%3C/svg%3E")}
|
|
@@ -345,10 +346,16 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
345
346
|
.thinking-body{margin-top:6px;font-size:13px;color:var(--text-3);white-space:pre-wrap;word-break:break-word;font-style:italic;max-height:240px;overflow:auto}
|
|
346
347
|
/* The turn block (the stage's cloud, the history): one block per turn, its steps as collapsed rows, the live one open on top. */
|
|
347
348
|
.turn-work{border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;background:var(--bg-panel);padding:8px 12px;font-size:14px}
|
|
348
|
-
.turn-work>summary{cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;user-select:none;list-style:none}
|
|
349
|
+
.turn-work>summary{position:relative;padding-left:13px;cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;user-select:none;list-style:none}
|
|
349
350
|
.turn-work>summary::-webkit-details-marker{display:none}
|
|
350
|
-
.turn-work>summary::before{content:"▸
|
|
351
|
-
|
|
351
|
+
.turn-work>summary::before{content:"▸";position:absolute;left:0;color:var(--accent)}
|
|
352
|
+
/* The summary's line: flex on this span, not on <summary> (WebKit's flex on a summary has had quirks). The chevron and the steps toggle the block, the footprint opens the turn's working set (working_set_view.js footClick). */
|
|
353
|
+
.tw-sum{display:flex;align-items:baseline;gap:10px;min-width:0}
|
|
354
|
+
/* The count gives way before the footprint, which is what the line is for. */
|
|
355
|
+
.ws-foot~.turn-steps{margin-left:auto;flex:0 100 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:12px;font-weight:500;color:var(--text-muted);white-space:nowrap}
|
|
356
|
+
/* A phone has no count to tap, and the chevron is then the only way to the steps: its hit area spreads over the block's padding, the glyph stays where it is. */
|
|
357
|
+
@media (max-width:600px){.ws-foot~.turn-steps{display:none}.turn-work>summary::before{left:-12px;top:-8px;width:36px;box-sizing:border-box;padding:8px 0 8px 12px}}
|
|
358
|
+
.turn-work[open]>summary::before{content:"▾"}
|
|
352
359
|
/* Closed, a block keeps its box (the bar, the border, the width): a click changes its height only (details_motion.js); thinking and the recap the same. */
|
|
353
360
|
/* A step's body (thinking, narration, rows, cards) sits --step-indent right of its summary, so the thinking
|
|
354
361
|
chevron and the row marks share one column under the step's own chevron. The summary takes it back with a
|
|
@@ -398,11 +405,11 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
398
405
|
.gen:not(.live) .gen-text{font-size:14px;color:var(--text-2)}
|
|
399
406
|
.gen-text:empty{display:none}
|
|
400
407
|
.gen .activity-body{margin-top:6px}
|
|
401
|
-
/* One gen, no thinking, no tool rows: nothing to stack yet, so the narration streams like a plain answer bubble. */
|
|
402
|
-
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)){border:0;padding:0;background:transparent}
|
|
403
|
-
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body))>summary,.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen>summary{display:none}
|
|
404
|
-
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen{margin:0;padding:0;border:0;background:transparent}
|
|
405
|
-
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen-text{margin:0;background:var(--bg-panel);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;padding:12px 14px;opacity:.95}
|
|
408
|
+
/* One gen, no thinking, no tool rows: nothing to stack yet, so the narration streams like a plain answer bubble. A step that closed with text (a line steered in after it, its answer gone from the block) is a row: its summary is the way to its text. */
|
|
409
|
+
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)){border:0;padding:0;background:transparent}
|
|
410
|
+
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text))>summary,.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen>summary{display:none}
|
|
411
|
+
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen{margin:0;padding:0;border:0;background:transparent}
|
|
412
|
+
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen-text{margin:0;background:var(--bg-panel);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;padding:12px 14px;opacity:.95}
|
|
406
413
|
@media(max-width:600px){.gen{--step-indent:8px}}
|
|
407
414
|
/* A question card asks for input like the composer card, so it wears the same glass (the recipe above); only its warm tint is its own. */
|
|
408
415
|
.bubble.question{border-left:3px solid var(--warn);background:color-mix(in srgb,var(--warn) 7%,var(--glass-bg));padding:10px 12px}
|
|
@@ -977,11 +984,11 @@ body.working-set .ts-live{position:relative;padding-right:264px}
|
|
|
977
984
|
}
|
|
978
985
|
body.working-set .turn-stage.waiting .ts-live{min-height:calc(var(--ts-live-h) + var(--ws-row-h,0px))}
|
|
979
986
|
body.working-set .turn-stage.held-wait:not(.waiting) .ts-live{min-height:max(calc(var(--ts-live-h) + var(--ws-row-h,0px)),var(--live-held,0px))}
|
|
980
|
-
/* A finished turn's footprint,
|
|
981
|
-
.ws-foot{
|
|
982
|
-
@media(hover:hover){.ws-foot:hover{
|
|
987
|
+
/* A finished turn's footprint, its block's summary line (working_set_view.js): a click opens its list. */
|
|
988
|
+
.ws-foot{flex:0 1 auto;min-width:0;padding:0 6px;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;font-weight:500;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
|
|
989
|
+
@media(hover:hover){.ws-foot:hover{background:var(--bg-hover)}}
|
|
983
990
|
.ws-foot:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
|
|
984
|
-
.ws-foot .w{font-family:ui-sans-serif,system-ui,sans-serif}
|
|
991
|
+
.ws-foot .w{font-family:ui-sans-serif,system-ui,sans-serif;font-weight:600;color:var(--text-muted)}
|
|
985
992
|
.ws-foot .sep{color:var(--border-strong)}
|
|
986
993
|
.ws-foot .e{color:var(--accent)}
|
|
987
994
|
.ws-foot .k{color:var(--think-text)}
|
|
@@ -1326,8 +1333,6 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
|
1326
1333
|
.us-row-head span{flex:1 1 0;min-width:0;text-align:right;color:var(--text-2);font-size:12px;overflow-wrap:anywhere}
|
|
1327
1334
|
.us-detail{margin-top:4px;color:var(--text-muted);font-size:12px;overflow-wrap:anywhere}
|
|
1328
1335
|
.us-kept{margin-top:6px;color:var(--warn-text);font-size:12px}
|
|
1329
|
-
.us-kept ul{margin:3px 0 0;padding-left:18px;color:var(--text-2)}
|
|
1330
|
-
.us-kept li{overflow-wrap:anywhere}
|
|
1331
1336
|
.us-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:10px}
|
|
1332
1337
|
@media(max-width:600px){#updateSheet{position:fixed;inset:auto 8px 8px;width:auto;max-width:none;max-height:min(80dvh,calc(100dvh - 16px));margin:0}}
|
|
1333
1338
|
.ctx-pop-kind{font-size:11px;color:var(--text-muted)}
|
|
@@ -1788,6 +1793,7 @@ a{color:var(--accent)}
|
|
|
1788
1793
|
<div id="stage">
|
|
1789
1794
|
<div id="markdownWarning" class="markdown-warning" hidden></div>
|
|
1790
1795
|
<div id="markdownTip" class="markdown-tip" hidden><span class="markdown-tip-text"></span><button type="button" class="markdown-tip-close" aria-label="Dismiss tip" title="Dismiss tip">×</button></div>
|
|
1796
|
+
<div id="objectivesStrip" hidden></div>
|
|
1791
1797
|
<div id="history"></div>
|
|
1792
1798
|
<div id="empty"></div>
|
|
1793
1799
|
<div id="notice" role="status" hidden></div>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* The session's objectives (objectives_strip.js, objectives_entry.js). Colours are tokens.css's only. */
|
|
2
|
+
#objectivesStrip{flex:none;padding:6px max(16px,calc((100% - 960px)/2));border-bottom:1px solid var(--border);background:color-mix(in srgb,var(--bg-panel) 60%,transparent)}
|
|
3
|
+
#objectivesStrip[hidden]{display:none}
|
|
4
|
+
.obj-strip{display:flex;align-items:center;gap:8px;min-height:32px}
|
|
5
|
+
.obj-chips{display:flex;flex-wrap:wrap;gap:6px;min-width:0;flex:1}
|
|
6
|
+
.obj-chip{display:inline-flex;align-items:baseline;gap:6px;max-width:280px;min-width:0;padding:3px 10px;border-radius:999px;border:1px solid var(--accent-border);background:var(--accent-bg);color:var(--text-2);font-size:13px;line-height:1.4}
|
|
7
|
+
.obj-chip.done{background:transparent;border-color:var(--border-strong);color:var(--text-muted)}
|
|
8
|
+
.obj-id{flex:none;font:700 12px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--accent)}
|
|
9
|
+
.obj-chip.done .obj-id{color:var(--text-muted)}
|
|
10
|
+
.obj-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
11
|
+
.obj-count{flex:none;color:var(--text-muted);font-size:12px;font-variant-numeric:tabular-nums}
|
|
12
|
+
/* An objectives note in the conversation: pills for a list, a line for an edit. */
|
|
13
|
+
.bubble.note.objectives-entry{display:flex;flex-wrap:wrap;align-items:center;gap:6px;background:transparent;border:0;padding:2px 0;white-space:normal}
|
|
14
|
+
.obj-entry-label{color:var(--text-muted);font-size:12px;margin-right:2px}
|
|
15
|
+
.obj-pill{display:inline-block;padding:2px 10px;border-radius:999px;border:1px solid var(--accent-border);background:var(--accent-bg);color:var(--text-2);font-size:13px;line-height:1.45}
|
|
16
|
+
.obj-pill.done{background:transparent;border-color:var(--border-strong);color:var(--text-muted)}
|
|
17
|
+
.bubble.note.objectives-entry.change{flex-wrap:nowrap;gap:10px;color:var(--text-muted);font-size:13px}
|
|
18
|
+
.obj-rule{flex:1 1 16px;min-width:16px;height:1px;background:var(--border-strong)}
|
|
19
|
+
.obj-change{min-width:0;text-align:center;overflow-wrap:anywhere}
|
|
20
|
+
/* The strip's chips open the edit list. */
|
|
21
|
+
button.obj-chip{cursor:pointer;font:inherit;font-size:13px;line-height:1.4;text-align:left}
|
|
22
|
+
@media(hover:hover){button.obj-chip:hover:not(:disabled){filter:none;border-color:var(--accent)}}
|
|
23
|
+
.obj-edit{flex:none;background:transparent;color:var(--text-muted);padding:3px 8px;font-size:12px}
|
|
24
|
+
@media(hover:hover){.obj-edit:hover:not(:disabled){filter:none;color:var(--text-bright)}}
|
|
25
|
+
.obj-waiting{flex:none;color:var(--warn-text);font-size:12px}
|
|
26
|
+
/* The edit list (chip_popover.js's shell, .ctx-popover; a bottom sheet on a phone). */
|
|
27
|
+
.obj-panel{display:flex;flex-direction:column;gap:10px;overflow:auto}
|
|
28
|
+
.obj-rows{display:flex;flex-direction:column;gap:2px}
|
|
29
|
+
.obj-row{display:flex;align-items:flex-start;gap:8px;padding:6px 0;border-top:1px solid var(--border)}
|
|
30
|
+
.obj-row:first-child{border-top:0}
|
|
31
|
+
.obj-row .obj-id{padding-top:3px}
|
|
32
|
+
.obj-row-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
33
|
+
.obj-row-text{display:block;width:100%;padding:2px 0;background:transparent;color:var(--text);text-align:left;font-size:14px;line-height:1.4;border-radius:4px;white-space:normal;overflow-wrap:anywhere}
|
|
34
|
+
@media(hover:hover){.obj-row-text:hover:not(:disabled){filter:none;text-decoration:underline dotted var(--text-muted)}}
|
|
35
|
+
.obj-row.done .obj-row-text{color:var(--text-muted)}
|
|
36
|
+
.obj-row.dropped .obj-row-text{color:var(--text-faint);text-decoration:line-through}
|
|
37
|
+
.obj-row.done .obj-id,.obj-row.dropped .obj-id{color:var(--text-muted)}
|
|
38
|
+
.obj-row-sub{display:flex;flex-direction:column;gap:1px;color:var(--text-muted);font-size:12px;overflow-wrap:anywhere}
|
|
39
|
+
.obj-row-acts{flex:none;display:flex;gap:4px}
|
|
40
|
+
.obj-check.refuted{color:var(--warn-text)}
|
|
41
|
+
.obj-check.confirmed{color:var(--text-2)}
|
|
42
|
+
.obj-act{padding:3px 10px;font-size:12px;border-radius:6px}
|
|
43
|
+
.obj-act.ghost{background:var(--bg-raised);color:var(--text-2)}
|
|
44
|
+
form.obj-row{align-items:center}
|
|
45
|
+
.obj-input{flex:1;min-width:0;padding:5px 8px;border-radius:6px;border:1px solid var(--border-strong);background:var(--bg-inset);color:var(--text);font-size:14px}
|
|
46
|
+
.obj-add{display:flex;gap:6px;align-items:center}
|
|
47
|
+
.obj-note{color:var(--text-muted);font-size:12px}
|
|
48
|
+
/* A phone: one line, the objective being worked on and how far along, and the whole list a tap away in a bottom sheet (chip_popover.js), never pushing the conversation down. */
|
|
49
|
+
@media(max-width:600px){
|
|
50
|
+
#objectivesStrip{padding:4px 12px}
|
|
51
|
+
.obj-strip{min-height:36px}
|
|
52
|
+
.obj-chips{flex-wrap:nowrap}
|
|
53
|
+
.obj-chip:not(.first){display:none}
|
|
54
|
+
.obj-chip.first{flex:1;max-width:none;min-height:36px;align-items:center;border-radius:10px}
|
|
55
|
+
.obj-edit{padding:8px;min-height:36px}
|
|
56
|
+
.obj-waiting{max-width:90px;font-size:11px;line-height:1.2}
|
|
57
|
+
.obj-pill{font-size:12.5px}
|
|
58
|
+
.bubble.note.objectives-entry.change{font-size:12.5px;gap:6px}
|
|
59
|
+
}
|
|
60
|
+
.obj-panel.sheet{font-size:15px;max-height:85dvh}
|
|
61
|
+
.obj-panel.sheet .obj-row{align-items:flex-start;flex-wrap:wrap;padding:8px 0}
|
|
62
|
+
.obj-panel.sheet form.obj-row{align-items:center;flex-wrap:nowrap}
|
|
63
|
+
/* The text keeps the row's width and the buttons get their own line: a phone reads a reason. */
|
|
64
|
+
.obj-panel.sheet .obj-row-main{flex:1 1 calc(100% - 40px)}
|
|
65
|
+
.obj-panel.sheet .obj-row-text{font-size:15px;min-height:32px}
|
|
66
|
+
.obj-panel.sheet .obj-row-acts{flex:1 1 100%;flex-wrap:wrap;justify-content:flex-end;gap:6px}
|
|
67
|
+
.obj-panel.sheet .obj-act{min-height:44px;min-width:56px;padding:0 8px;font-size:14px;border-radius:10px}
|
|
68
|
+
.obj-panel.sheet .obj-input{min-height:44px;font-size:16px;border-radius:10px}
|
|
69
|
+
.obj-panel.sheet .obj-note{font-size:12px}
|
|
70
|
+
@media (prefers-reduced-motion:reduce){.obj-chip,.obj-act{transition:none}}
|
|
71
|
+
/* A move: a turn's line above its answer, a tool row's chip, a decision card's "Concerns". */
|
|
72
|
+
.obj-pill.obj-mark{padding:0 7px;font:700 11.5px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--accent)}
|
|
73
|
+
.obj-moves{display:flex;flex-direction:column;gap:2px;margin-bottom:-4px;font-size:13px}
|
|
74
|
+
.obj-move-line{display:flex;align-items:baseline;gap:8px;color:var(--text-2);flex-wrap:wrap}
|
|
75
|
+
.obj-move-note{min-width:0;flex:1 1 200px;overflow-wrap:anywhere}
|
|
76
|
+
.obj-concerns .obj-pill{margin-right:4px}
|
|
77
|
+
.obj-notyet{flex:none}
|
|
78
|
+
.obj-moves:not(.newest) .obj-notyet{display:none}
|
|
79
|
+
@media (pointer:coarse){.obj-notyet{min-height:36px}}
|
|
80
|
+
.obj-short{display:flex;justify-content:flex-end;margin-top:-6px}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// An objectives note in the conversation, in place of the plain note bubble
|
|
2
|
+
// (turn_html.js noteBubbleTemplate): the list as pills, or, for a later edit, one
|
|
3
|
+
// line saying what the user changed. Read-only: the strip above the history is
|
|
4
|
+
// where the list is edited (objectives_strip.js). The raw note text stays the
|
|
5
|
+
// tooltip. A module of its own: static JS is cached at unversioned URLs.
|
|
6
|
+
import { html, nothing } from "./vendor/lit-html.js";
|
|
7
|
+
import { DROPPED, entryLine } from "./objectives_model.js";
|
|
8
|
+
|
|
9
|
+
function pill(item) {
|
|
10
|
+
const klass = `obj-pill ${item.status}`;
|
|
11
|
+
return html`<span class=${klass} title=${item.done_when ? `done when: ${item.done_when}` : nothing}><span class="obj-id">${item.id}</span> ${item.status === "done" ? "✓ " : ""}${item.text}</span>`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function objectivesEntryTemplate(note, timeTitle = "") {
|
|
15
|
+
const line = entryLine(note.objectives);
|
|
16
|
+
const tip = [timeTitle, note.content].filter(Boolean).join("\n");
|
|
17
|
+
if (line) {
|
|
18
|
+
return html`<div class="bubble note objectives-entry change" title=${tip}><span class="obj-rule" aria-hidden="true"></span><span class="obj-change">${line}</span><span class="obj-rule" aria-hidden="true"></span></div>`;
|
|
19
|
+
}
|
|
20
|
+
const items = (note.objectives?.items || []).filter((i) => i.status !== DROPPED);
|
|
21
|
+
return html`<div class="bubble note objectives-entry" title=${tip}><span class="obj-entry-label">Objectives</span>${items.map(pill)}</div>`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// The session's objectives as the page reads them (pure, no DOM): the fold the
|
|
2
|
+
// server sends (Objectives.view: {items: [{id, text, status, done_when?,
|
|
3
|
+
// move?, check?}], pending}) and what a list note's entry says (Objectives.entry:
|
|
4
|
+
// {items, changes?}). See docs/internals/objectives.md.
|
|
5
|
+
|
|
6
|
+
export const ACTIVE = "active";
|
|
7
|
+
export const DONE = "done";
|
|
8
|
+
export const DROPPED = "dropped";
|
|
9
|
+
|
|
10
|
+
const itemsOf = (list) => (Array.isArray(list?.items) ? list.items : []);
|
|
11
|
+
|
|
12
|
+
// The objectives the strip shows: active first, then done; a dropped one only
|
|
13
|
+
// in the edit list.
|
|
14
|
+
export function stripItems(list) {
|
|
15
|
+
const items = itemsOf(list);
|
|
16
|
+
return [...items.filter((i) => i.status === ACTIVE), ...items.filter((i) => i.status === DONE)];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Done of the ones still meant (a dropped one doesn't count).
|
|
20
|
+
export function progress(list) {
|
|
21
|
+
const kept = itemsOf(list).filter((i) => i.status !== DROPPED);
|
|
22
|
+
return { done: kept.filter((i) => i.status === DONE).length, total: kept.length };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The one objective a one-line strip names: the first still active, else the
|
|
26
|
+
// last one done.
|
|
27
|
+
export function headline(list) {
|
|
28
|
+
const items = stripItems(list);
|
|
29
|
+
return items.find((i) => i.status === ACTIVE) || items[items.length - 1] || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// One change as words after "You": "marked O2 done", "reworded O3: old → new".
|
|
33
|
+
export function changeText(change) {
|
|
34
|
+
const { id, kind, text, was } = change || {};
|
|
35
|
+
switch (kind) {
|
|
36
|
+
case "added": return `added ${id} ${text}`;
|
|
37
|
+
case "reworded": return `reworded ${id}: ${was} → ${text}`;
|
|
38
|
+
case DONE: return `marked ${id} done`;
|
|
39
|
+
case DROPPED: return `dropped ${id} ${text}`;
|
|
40
|
+
case "reopened": return `reopened ${id}`;
|
|
41
|
+
default: return String(id ?? "");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// An entry's line: what the user did, or null when it has nothing to say (a
|
|
46
|
+
// first list, a list sent again), which then shows the list itself.
|
|
47
|
+
export function entryLine(entry) {
|
|
48
|
+
const changes = Array.isArray(entry?.changes) ? entry.changes : [];
|
|
49
|
+
return changes.length ? `You ${changes.map(changeText).join(" · ")}` : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The one route to an edit: the lines /objectives takes (ObjectivesCommand).
|
|
53
|
+
const oneLine = (text) => String(text ?? "").replace(/\s+/g, " ").trim();
|
|
54
|
+
export const markLine = (verb, id) => `/objectives ${verb} ${id}`;
|
|
55
|
+
export const addLine = (text) => (oneLine(text) ? `/objectives add ${oneLine(text)}` : null);
|
|
56
|
+
export const rewordLine = (id, text) => (oneLine(text) ? `/objectives edit ${id} ${oneLine(text)}` : null);
|
|
57
|
+
|
|
58
|
+
// An item's text as the edit box shows it: the form /objectives reads back.
|
|
59
|
+
export function editableText(item) {
|
|
60
|
+
return item?.done_when ? `${item.text} (done when: ${item.done_when})` : String(item?.text ?? "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// What the strip says about an edit waiting for the running turn's end. Idle,
|
|
64
|
+
// it lands in a moment and says nothing.
|
|
65
|
+
export function pendingText(list, running) {
|
|
66
|
+
return list?.pending && running ? "Applies after this turn" : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// A check's line in the list: its verdict and reason, or that it runs. Information
|
|
70
|
+
// only (ObjectivesCheck). null: no check of the latest move.
|
|
71
|
+
export function checkText(check) {
|
|
72
|
+
if (!check) return null;
|
|
73
|
+
if (check.running) return "Checking…";
|
|
74
|
+
const word = check.verdict === "no_answer" ? "no answer" : check.verdict;
|
|
75
|
+
return `Check: ${word}${check.reason ? ` — ${check.reason}` : ""}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// When the soonest running check runs out, in ms; null with none. No event says so
|
|
79
|
+
// when its worker dies.
|
|
80
|
+
export function nextExpiry(list) {
|
|
81
|
+
const times = itemsOf(list).filter((i) => i.check?.running && i.check.until).map((i) => Date.parse(i.check.until)).filter(Number.isFinite);
|
|
82
|
+
return times.length ? Math.min(...times) : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// +list+ without the running checks past their time.
|
|
86
|
+
export function expireChecks(list, now) {
|
|
87
|
+
if (!list) return list;
|
|
88
|
+
const gone = (i) => i.check?.running && Date.parse(i.check.until) <= now;
|
|
89
|
+
if (!itemsOf(list).some(gone)) return list;
|
|
90
|
+
return { ...list, items: list.items.map((i) => {
|
|
91
|
+
if (!gone(i)) return i;
|
|
92
|
+
const { check: _gone, ...rest } = i;
|
|
93
|
+
return rest;
|
|
94
|
+
}) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// What a tap on Not yet tells the agent: a normal message, as if typed.
|
|
98
|
+
export function notYetText(item) {
|
|
99
|
+
return `Not yet on ${item.id} (${item.text}).`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The model's moves on the objectives, as chips in the conversation. A tool row
|
|
2
|
+
// of the objectives tool shows its id as a chip (moveRowText, turn_html.js, live
|
|
3
|
+
// and reloaded alike), but a finished turn's steps collapse, so a finished turn
|
|
4
|
+
// also gets a line of its moves between its block and its answer (mockup G). The
|
|
5
|
+
// rows are the record: one observer on the history reads them back, as
|
|
6
|
+
// answer_fold.js decorates its answers, so every path that fills it is covered.
|
|
7
|
+
// Nothing is drawn for a turn with no move. A decision-card's "Concerns O2" gets
|
|
8
|
+
// the same chip. The newest long answer of a session with a list offers its
|
|
9
|
+
// short version where the bundle's /short is there (decision-card): the card
|
|
10
|
+
// lands under it, so the button sits there too. A module of its own: static JS is cached at unversioned URLs.
|
|
11
|
+
import { foldable } from "./answer_fold.js";
|
|
12
|
+
import { keepFollowing } from "./scroll.js";
|
|
13
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
14
|
+
|
|
15
|
+
const ROW_TITLE = /^(O\d+)\s+([^]*)$/;
|
|
16
|
+
const RECORDED = /Recorded a move on /;
|
|
17
|
+
const CONCERNS = /^Concerns (O\d+)\b/;
|
|
18
|
+
|
|
19
|
+
// A row's move: {id, note}, null for any other row, and for a call that recorded
|
|
20
|
+
// nothing (no list, a refusal: Tools::ObjectivesTool).
|
|
21
|
+
export function moveOfRow(row) {
|
|
22
|
+
if (row?.tool !== "objectives" || !RECORDED.test(String(row.output || ""))) return null;
|
|
23
|
+
const found = ROW_TITLE.exec(String(row.title || row.params || ""));
|
|
24
|
+
return found ? { id: found[1], note: found[2] } : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The row's title, its objective a chip, marked for the line above an answer
|
|
28
|
+
// when the call recorded a move.
|
|
29
|
+
export function moveRowText(row, shown) {
|
|
30
|
+
if (row?.tool !== "objectives") return shown;
|
|
31
|
+
const found = ROW_TITLE.exec(String(shown ?? ""));
|
|
32
|
+
if (!found) return shown;
|
|
33
|
+
const move = moveOfRow(row);
|
|
34
|
+
return html`<span class="obj-pill obj-mark" data-move-id=${move ? move.id : nothing} data-move-note=${move ? move.note : nothing}>${found[1]}</span> ${found[2]}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A finished turn block's moves, in order.
|
|
38
|
+
export function movesInBlock(block) {
|
|
39
|
+
return [...block.querySelectorAll("[data-move-id]")].map((el) => ({ id: el.dataset.moveId, note: el.dataset.moveNote || "" }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A card body's "Concerns O2 …" paragraph: the id a chip. Idempotent.
|
|
43
|
+
export function chipConcerns(body) {
|
|
44
|
+
for (const p of body.querySelectorAll("p")) {
|
|
45
|
+
const first = p.firstChild;
|
|
46
|
+
if (first?.nodeType !== 3) continue;
|
|
47
|
+
const found = CONCERNS.exec(first.textContent);
|
|
48
|
+
if (!found) continue;
|
|
49
|
+
const chip = body.ownerDocument.createElement("span");
|
|
50
|
+
chip.className = "obj-pill obj-mark";
|
|
51
|
+
chip.textContent = found[1];
|
|
52
|
+
first.textContent = first.textContent.slice(found[0].length);
|
|
53
|
+
p.insertBefore(chip, first);
|
|
54
|
+
p.classList.add("obj-concerns");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function lineFor(block) {
|
|
59
|
+
const next = block.nextElementSibling;
|
|
60
|
+
return next?.classList.contains("obj-moves") ? next : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// @param historyEl the history
|
|
64
|
+
// @param notYet (id) the tap on a move's Not yet (the newest turn's only: CSS)
|
|
65
|
+
// @param canShorten () whether the newest long answer offers its short version
|
|
66
|
+
// @param shorten () the tap on it
|
|
67
|
+
export function createObjectivesMoves({ historyEl, notYet = () => {}, canShorten = () => false, shorten = () => {} }) {
|
|
68
|
+
// Off once tapped; back when +ask+ answers false (a failure, already told).
|
|
69
|
+
const once = (e, ask) => {
|
|
70
|
+
const button = e.currentTarget;
|
|
71
|
+
button.disabled = true;
|
|
72
|
+
Promise.resolve(ask()).then((sent) => {
|
|
73
|
+
if (sent === false) button.disabled = false;
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const drawLine = (line, moves) => render(html`${moves.map((m) => html`<div class="obj-move-line">
|
|
77
|
+
<span class="obj-pill obj-mark">${m.id}</span><span class="obj-move-note">${m.note}</span>
|
|
78
|
+
<button type="button" class="obj-act ghost obj-notyet" title=${`Tell the agent: not yet on ${m.id}`} @click=${(e) => once(e, () => notYet(m.id))}>Not yet</button>
|
|
79
|
+
</div>`)}`, line);
|
|
80
|
+
|
|
81
|
+
const markNewest = () => {
|
|
82
|
+
const lines = [...historyEl.querySelectorAll(":scope > .obj-moves")];
|
|
83
|
+
lines.forEach((line, i) => line.classList.toggle("newest", i === lines.length - 1));
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// After an answer's own rows (its Revise row, its timing line: the bubble's
|
|
87
|
+
// next siblings, one of which CSS reads by adjacency).
|
|
88
|
+
function shortRowPlace(answer) {
|
|
89
|
+
let place = answer;
|
|
90
|
+
while (place.nextElementSibling?.matches(".revise-row, .turn-timing")) place = place.nextElementSibling;
|
|
91
|
+
return place;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function placeShort() {
|
|
95
|
+
const answers = canShorten() ? historyEl.querySelectorAll(":scope > .bubble.output:not(.streaming)") : [];
|
|
96
|
+
const answer = answers[answers.length - 1];
|
|
97
|
+
const long = answer && foldable(answer.dataset.copySource || answer.textContent);
|
|
98
|
+
let row = historyEl.querySelector(":scope > .obj-short");
|
|
99
|
+
if (!long) {
|
|
100
|
+
row?.remove();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!row) {
|
|
104
|
+
row = historyEl.ownerDocument.createElement("div");
|
|
105
|
+
row.className = "obj-short";
|
|
106
|
+
render(html`<button type="button" class="obj-act ghost" title="Ask for the short version; nothing enters the conversation"
|
|
107
|
+
@click=${(e) => once(e, shorten)}>Short version</button>`, row);
|
|
108
|
+
}
|
|
109
|
+
const place = shortRowPlace(answer);
|
|
110
|
+
if (place.nextElementSibling !== row) {
|
|
111
|
+
row.querySelector("button").disabled = false;
|
|
112
|
+
keepFollowing(historyEl, () => place.after(row));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function decorate() {
|
|
117
|
+
for (const block of historyEl.querySelectorAll(":scope > .turn-work")) {
|
|
118
|
+
const moves = movesInBlock(block);
|
|
119
|
+
const line = lineFor(block);
|
|
120
|
+
if (!moves.length) {
|
|
121
|
+
line?.remove();
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const sig = JSON.stringify(moves);
|
|
125
|
+
if (line?.dataset.sig === sig) continue;
|
|
126
|
+
keepFollowing(historyEl, () => {
|
|
127
|
+
const el = line || historyEl.ownerDocument.createElement("div");
|
|
128
|
+
el.className = "obj-moves";
|
|
129
|
+
el.dataset.sig = sig;
|
|
130
|
+
drawLine(el, moves);
|
|
131
|
+
if (!line) block.after(el);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
markNewest();
|
|
135
|
+
placeShort();
|
|
136
|
+
for (const card of historyEl.querySelectorAll(".plugin-card .card-body")) {
|
|
137
|
+
if (card.closest(".plugin-card").querySelector(".card-source")?.textContent === "decision-card") chipConcerns(card);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The class toggle isn't a childList change, so this doesn't feed itself.
|
|
142
|
+
new MutationObserver(decorate).observe(historyEl, { childList: true, subtree: true });
|
|
143
|
+
decorate();
|
|
144
|
+
return { decorate };
|
|
145
|
+
}
|