samagotchi 0.83.0 → 0.85.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/bin/chi +27 -2
  4. data/docs/architecture.md +10 -1
  5. data/docs/cli.md +17 -1
  6. data/docs/configuration.md +2 -0
  7. data/docs/hooks.md +1 -1
  8. data/docs/internals/bundles.md +1 -1
  9. data/docs/internals/engine.md +3 -2
  10. data/docs/plugins.md +31 -5
  11. data/docs/sessions.md +15 -2
  12. data/docs/sub-agent.md +5 -1
  13. data/lib/samagotchi/bridge/turn_accumulator.rb +3 -0
  14. data/lib/samagotchi/bridge.rb +54 -1
  15. data/lib/samagotchi/bridge_client.rb +12 -0
  16. data/lib/samagotchi/bundle_session_settings.rb +212 -0
  17. data/lib/samagotchi/bundles/cost-cap/manifest.yml +4 -2
  18. data/lib/samagotchi/bundles/cost-cap/plugin.rb +12 -5
  19. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  20. data/lib/samagotchi/engine.rb +1 -1
  21. data/lib/samagotchi/kernel_loop.rb +5 -1
  22. data/lib/samagotchi/llm/chat_loop.rb +5 -1
  23. data/lib/samagotchi/log_subscriber.rb +1 -0
  24. data/lib/samagotchi/memory_bundle/builder.rb +3 -2
  25. data/lib/samagotchi/memory_bundle/installed_bundle.rb +6 -3
  26. data/lib/samagotchi/memory_bundle/installer.rb +2 -1
  27. data/lib/samagotchi/memory_bundle/manifest.rb +24 -2
  28. data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
  29. data/lib/samagotchi/parent_report.rb +5 -2
  30. data/lib/samagotchi/plugin/context.rb +36 -2
  31. data/lib/samagotchi/reply_wait.rb +8 -1
  32. data/lib/samagotchi/send_command.rb +24 -7
  33. data/lib/samagotchi/session.rb +16 -1
  34. data/lib/samagotchi/session_commands.rb +7 -4
  35. data/lib/samagotchi/session_inbox.rb +68 -0
  36. data/lib/samagotchi/session_setup.rb +14 -6
  37. data/lib/samagotchi/terminal_ui/attach_launcher.rb +8 -5
  38. data/lib/samagotchi/terminal_ui/attached_loop.rb +89 -1
  39. data/lib/samagotchi/terminal_ui.rb +10 -2
  40. data/lib/samagotchi/testing/bundle.rb +31 -5
  41. data/lib/samagotchi/version.rb +1 -1
  42. data/lib/samagotchi/web/app.rb +41 -4
  43. data/lib/samagotchi/web/public/app.js +17 -0
  44. data/lib/samagotchi/web/public/bubbles.js +9 -0
  45. data/lib/samagotchi/web/public/commands.js +1 -0
  46. data/lib/samagotchi/web/public/data.js +11 -0
  47. data/lib/samagotchi/web/public/index.html +9 -0
  48. data/lib/samagotchi/web/public/stage_view.js +6 -0
  49. data/lib/samagotchi/web/public/stream_handlers.js +3 -0
  50. data/lib/samagotchi/web/public/turn_events.js +4 -0
  51. data/lib/samagotchi/web/public/turn_html.js +9 -1
  52. data/lib/samagotchi/web/public/unqueue.js +56 -0
  53. metadata +3 -1
@@ -166,6 +166,7 @@ module Samagotchi
166
166
  ["GET", %r{\A/api/sessions/([^/]+)/output\z}, :handle_output],
167
167
  ["POST", %r{\A/api/sessions/([^/]+)/cancel\z}, :handle_cancel],
168
168
  ["POST", %r{\A/api/sessions/([^/]+)/tasks/([^/]+)/stop\z}, :handle_task_stop],
169
+ ["POST", %r{\A/api/sessions/([^/]+)/turn/([^/]+)/dequeue\z}, :handle_dequeue_turn],
169
170
  ["POST", %r{\A/api/sessions/([^/]+)/stop\z}, :handle_stop],
170
171
  ["POST", %r{\A/api/sessions/([^/]+)/restart\z}, :handle_restart],
171
172
  ["POST", %r{\A/api/sessions/([^/]+)/archive\z}, :handle_archive],
@@ -649,6 +650,15 @@ module Samagotchi
649
650
  thinking, error = request_thinking(body["thinking"])
650
651
  return error if error
651
652
 
653
+ # bundle_settings: the session's own bundle settings (chi --set:
654
+ # {bundle => {key => scalar}}), checked against the installed
655
+ # bundles the same way.
656
+ begin
657
+ bundle_settings = BundleSessionSettings.from_request(body["bundle_settings"])
658
+ rescue BundleSessionSettings::Error => e
659
+ return error_response(400, "invalid_bundle_settings", e.message)
660
+ end
661
+
652
662
  folder = dir ? { working_directory: dir } : {}
653
663
  # model: the model the session starts on (the start page's picker,
654
664
  # spelled as GET /api/models lists it); blank means the default.
@@ -659,7 +669,7 @@ module Samagotchi
659
669
  # until that turn is saved.
660
670
  preview = idle ? body["preview"].to_s.strip : ""
661
671
  folder[:title] = preview unless preview.empty?
662
- setup = SessionSetup.new(llm_context: llm_context, thinking: thinking)
672
+ setup = SessionSetup.new(llm_context: llm_context, thinking: thinking, bundle_settings: bundle_settings)
663
673
  folder[:setup] = setup unless setup.empty?
664
674
  begin
665
675
  session = @manager.spawn_session(prompt: idle ? nil : prompt.to_s, state_dir: @state_dir, **folder)
@@ -679,7 +689,7 @@ module Samagotchi
679
689
  # POST /api/sessions with continues: <id, prefix or last:id>: the next
680
690
  # link of that session's chain (SessionManager.continue_session), in its
681
691
  # folder and with its own setup (SessionSetup: llm_context, thinking
682
- # and reasoning_history levels), so dir, llm_context and thinking are
692
+ # and reasoning_history levels, bundle settings), so dir, llm_context, thinking and bundle_settings are
683
693
  # refused beside it (a create takes no reasoning_history). It runs on
684
694
  # model (or model_name) when given, else on the previous link's: one
685
695
  # whose host is gone falls back to the default, and the 201's
@@ -694,9 +704,9 @@ module Samagotchi
694
704
  unless given.is_a?(String) && !given.strip.empty?
695
705
  return error_response(400, "invalid_continues", "continues is a session id")
696
706
  end
697
- if (fields = %w[dir llm_context thinking].select { |key| body.key?(key) }).any?
707
+ if (fields = %w[dir llm_context thinking bundle_settings].select { |key| body.key?(key) }).any?
698
708
  return error_response(400, "invalid_continues",
699
- "continues takes the previous session's folder, llm_context and thinking; " \
709
+ "continues takes the previous session's folder, llm_context, thinking and bundle_settings; " \
700
710
  "leave out #{fields.join(", ")}")
701
711
  end
702
712
 
@@ -1330,6 +1340,33 @@ module Samagotchi
1330
1340
  error_response(404, "not_found", e.message)
1331
1341
  end
1332
1342
 
1343
+ # POST /api/sessions/:id/turn/:enqueued_id/dequeue {client_id?}: the ×
1344
+ # on a queued bubble takes the message back before it runs
1345
+ # (Bridge#handle_dequeue_turn). Straight to the live worker, never
1346
+ # through deliver_turn: a removal must not wake one, and a stopped
1347
+ # worker's queued file is the next worker's (503 not_live). 200
1348
+ # {status, enqueued_id, prompt}; 409 already_started / not_queued and
1349
+ # 404 unknown_enqueued_id pass through; the rest as every relay.
1350
+ def handle_dequeue_turn(req, id, enqueued_id)
1351
+ @session_class.load(id, state_dir: default_state_dir)
1352
+ body = request_body(req)
1353
+ parsed = body.nil? || body.strip.empty? ? nil : parse_json(body)
1354
+ client_id = (parsed["client_id"] if parsed.is_a?(Hash))
1355
+
1356
+ request = ->(client) { client.dequeue(enqueued_id: enqueued_id, client_id: client_id) }
1357
+ relay(id, bridge_client(id), request, what: "the message was not removed", cant: "remove queued messages") do |reply|
1358
+ body = reply.json || {}
1359
+ case reply.status
1360
+ when 200 then json_response(200, { status: body["status"], enqueued_id: enqueued_id, prompt: body["prompt"].to_s, session_id: id })
1361
+ when 409 then json_response(409, { error: body["error"] || "already_started", detail: body["detail"], enqueued_id: enqueued_id, session_id: id }.compact)
1362
+ when 404
1363
+ json_response(404, { error: "unknown_enqueued_id", enqueued_id: enqueued_id, session_id: id }) if body["error"] == "unknown_enqueued_id"
1364
+ end
1365
+ end
1366
+ rescue ArgumentError => e
1367
+ error_response(404, "not_found", e.message)
1368
+ end
1369
+
1333
1370
  def handle_stop(_req, id)
1334
1371
  # Bounded, so a Rack thread isn't held long; a worker still exiting
1335
1372
  # after it just means an immediate resume finds it (rare).
@@ -92,6 +92,7 @@ import { currentGen } from "./turn_model.js";
92
92
  import { toolName } from "./activity.js";
93
93
  import { toastBottom } from "./toast_place.js";
94
94
  import { createToasts } from "./toasts.js";
95
+ import { createUnqueue } from "./unqueue.js";
95
96
 
96
97
  const $ = (s) => document.querySelector(s);
97
98
  const topStripEl = $("#topStrip");
@@ -1801,6 +1802,22 @@ composerEl.addEventListener("drop", (e) => {
1801
1802
  e.preventDefault();
1802
1803
  addImageFiles(files);
1803
1804
  });
1805
+ // The × on a message queued for after the turn takes it back (unqueue.js).
1806
+ const unqueue = createUnqueue({
1807
+ sessionId: () => selected,
1808
+ clientId,
1809
+ toast: (text) => showToast(text),
1810
+ // Edit: the removed text back into an empty composer (never over a draft).
1811
+ putBack: (text) => {
1812
+ if (promptEl.value.trim() || chips.length) return false;
1813
+ openForVisit();
1814
+ promptEl.value = text;
1815
+ fitPrompt();
1816
+ promptEl.focus();
1817
+ return true;
1818
+ },
1819
+ });
1820
+ turnRoots().forEach((root) => root.addEventListener("click", (e) => { unqueue.onClick(e); }));
1804
1821
  turnRoots().forEach((root) => root.addEventListener("click", (e) => {
1805
1822
  const img = e.target.closest("img.thumb");
1806
1823
  if (!img) return;
@@ -13,6 +13,13 @@ import { linkedLine } from "./notice_links.js";
13
13
  import { hookNoticeLabel, isReportClient, promptOps } from "./turn_events.js";
14
14
  import { drawCommandBubble } from "./commands.js";
15
15
 
16
+ export function removeQueuedBubble(bubble) {
17
+ if (!bubble) return;
18
+ const slot = bubble.parentElement?.classList.contains("ts-queued") ? bubble.parentElement : null;
19
+ bubble.remove();
20
+ if (slot && slot.childElementCount === 0) slot.remove();
21
+ }
22
+
16
23
  // A prompt that started a turn: not a line steered into a running one.
17
24
  export const TURN_PROMPT = ".bubble.user:not(.steered)";
18
25
 
@@ -205,6 +212,8 @@ export function createBubbles({
205
212
  setBubbleState(bubble, null);
206
213
  started = bubble;
207
214
  }
215
+ } else if (op.op === "remove") {
216
+ removeQueuedBubble(bubbleById(op.enqueuedId));
208
217
  } else if (op.op === "steer") {
209
218
  const bubble = bubbleById(op.enqueuedId);
210
219
  if (bubble) setBubbleState(bubble, "steered");
@@ -110,6 +110,7 @@ const WEB_LOCAL_REPLIES = {
110
110
  "/exit": "/exit: close the tab to leave; stop in the session bar stops the worker",
111
111
  "/quit": "/quit: close the tab to leave; stop in the session bar stops the worker",
112
112
  "/recap": "/recap: not in the web yet; a recap shows here by itself when you come back to an idle session, and /recap in a terminal (chi --attach) makes one now",
113
+ "/unqueue": "/unqueue: a terminal's command; here the × on a message queued for after the turn removes it",
113
114
  "/stats": "/stats: not in the web yet; each turn shows its time, the session bar the context use and speed (hover ctx for tokens and cost), and /stats in a terminal (chi --attach) has the rest",
114
115
  };
115
116
 
@@ -169,6 +169,17 @@ export function stopTask(id, taskId, opts = {}) {
169
169
  });
170
170
  }
171
171
 
172
+ // Takes back a message queued for after the running turn (the × on its
173
+ // bubble): 200 {status, enqueued_id, prompt}; 409 already_started /
174
+ // not_queued, 404 unknown_enqueued_id, 503 no live worker.
175
+ export function dequeueTurn(id, enqueuedId, { clientId = null } = {}, opts = {}) {
176
+ return api(`/api/sessions/${encodeURIComponent(id)}/turn/${encodeURIComponent(enqueuedId)}/dequeue`, {
177
+ method: "POST",
178
+ body: JSON.stringify(clientId ? { client_id: clientId } : {}),
179
+ ...opts,
180
+ });
181
+ }
182
+
172
183
  // Hands the session to a new worker on the newest chi installed: 200
173
184
  // {status, from_version, version}, or 409 with the reason in words (detail).
174
185
  export function restartSession(id, opts = {}) {
@@ -120,6 +120,12 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
120
120
  .bubble.user.queued .state-badge::before{background:var(--accent);box-shadow:0 0 0 1px var(--accent-ring)}
121
121
  .bubble.user.steered .state-badge::before{background:var(--accent);box-shadow:0 0 0 1px var(--accent-ring)}
122
122
  .bubble.user.failed .state-badge::before{background:var(--danger);box-shadow:0 0 0 1px color-mix(in srgb,var(--danger) 40%,transparent)}
123
+ /* The × on a message queued for after the turn (unqueue.js): only once the bubble has its enqueued_id; a 32px tap target around a small mark */
124
+ .bubble.user .state-badge button.unqueue{position:relative;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;margin:0 -3px 0 0;padding:0;border:0;border-radius:50%;background:transparent;color:var(--text-muted);font-size:14px;line-height:1;cursor:pointer}
125
+ .bubble.user .state-badge button.unqueue::before{content:"\00d7"}
126
+ .bubble.user .state-badge button.unqueue::after{content:"";position:absolute;inset:-8px}
127
+ .bubble.user .state-badge button.unqueue:hover:not(:disabled){background:var(--danger-bg-dim);color:var(--danger-text);filter:none}
128
+ .bubble.user:not([data-enqueued-id]) .state-badge button.unqueue{display:none}
123
129
  .bubble.user .origin-label{font-size:11px;color:var(--text-3);text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}
124
130
  .bubble.user .user-message{flex:1;min-width:0}
125
131
  .bubble.user blockquote{border-left:3px solid var(--border-strong);padding-left:10px;color:var(--text-3);margin:2px 0 6px}
@@ -847,6 +853,9 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
847
853
  .turn-stage .turn-work.outline>.gen.folded{display:block}
848
854
  .turn-stage .ts-cloud-body .gen:first-of-type{margin-top:0;border-top:0}
849
855
  @media(max-width:600px){
856
+ .bubble.user.queued[data-delivery="queue"]{flex-wrap:wrap}
857
+ .bubble.user.queued[data-delivery="queue"] .origin-label,.bubble.user.queued[data-delivery="queue"] .state-badge{order:0}
858
+ .bubble.user.queued[data-delivery="queue"] .user-message{order:1;flex-basis:100%}
850
859
  .ts-status{padding-left:10px}
851
860
  .ts-scroll{padding:8px 10px 10px}
852
861
  .ts-foot{padding:0 10px 10px}
@@ -410,6 +410,10 @@ export function createStageView({
410
410
  }
411
411
  }
412
412
 
413
+ function ensureQueuedSlot() {
414
+ if (!queuedSlot.isConnected) stage.appendChild(queuedSlot);
415
+ }
416
+
413
417
  function emptySlots() {
414
418
  for (const slot of [promptSlot, extras, answer, tail, cloudBody, queuedSlot, timingSlot]) slot.replaceChildren();
415
419
  slotsView.reset();
@@ -535,6 +539,7 @@ export function createStageView({
535
539
  // The lines still queued behind this turn went into the history with
536
540
  // the turn before (the hand-off at this turn_started, or an earlier
537
541
  // one): back below this turn's card, as a reload places them.
542
+ ensureQueuedSlot();
538
543
  queuedSlot.append(...waiting.filter((el) => !el.isConnected), ...historyEl.querySelectorAll(QUEUED_CHILDREN));
539
544
  view.turnStarted(data || {});
540
545
  show();
@@ -616,6 +621,7 @@ export function createStageView({
616
621
  const where = placeFor({ live: holding, handingOff: false, kind: placeKind(el) });
617
622
  if (where === "history") return false;
618
623
  if (where === "queued") {
624
+ ensureQueuedSlot();
619
625
  queuedSlot.appendChild(el);
620
626
  scheduleRefresh();
621
627
  return true;
@@ -116,6 +116,9 @@ export function createStreamHandlers(deps) {
116
116
  showSteers(data.steers);
117
117
  },
118
118
  turn_enqueued: (data) => applyPromptOps(data),
119
+ // A queued message taken back before it ran (the × here or in another
120
+ // tab, /unqueue in a terminal): its bubble goes, in every tab.
121
+ turn_dequeued: (data) => applyPromptOps(data),
119
122
  turn_started: (data) => keepFollowing(historyEl, () => {
120
123
  if (stream.takeGuess()) {
121
124
  // A replay of a turn the render drew: re-read and stream from there.
@@ -247,6 +247,8 @@ export function initWaitLine({ tasks } = {}) {
247
247
  // tag — this tab's own local echo gets its enqueued_id
248
248
  // start — the bubble's turn started (drops the "queued" badge)
249
249
  // steer — the bubble was merged into the running turn
250
+ // remove — the queued message was taken back before it ran (turn_dequeued,
251
+ // whoever removed it): its bubble goes
250
252
  // report — the merge brought a delegate child's report (origin
251
253
  // child:<id8>, no bubble anywhere): its text gets a labelled one
252
254
  // `known(enqueuedId)` says whether a bubble carries that id already;
@@ -276,6 +278,8 @@ export function promptOps(event, { myId, known, unmatchedMerge = false, wakeStar
276
278
  }
277
279
  return [withImages({ op: "add", enqueuedId: id, prompt: event.prompt, state: "started", label: clientLabel(origin.client_id) }, event.images)];
278
280
  }
281
+ case "turn_dequeued":
282
+ return event.enqueued_id ? [{ op: "remove", enqueuedId: event.enqueued_id }] : [];
279
283
  case "input_merged": {
280
284
  const ops = (event.origins || []).filter((o) => o && o.enqueued_id).map((o) =>
281
285
  known(o.enqueued_id) ? { op: "steer", enqueuedId: o.enqueued_id } : { op: "steer", enqueuedId: o.enqueued_id, unmatched: true },
@@ -184,9 +184,17 @@ export function badgeText(state, delivery = null) {
184
184
  return (state === "queued" && QUEUED_BADGES[delivery]) || BADGES[state];
185
185
  }
186
186
 
187
+ // A message queued for after the turn (delivery queue) can be taken back
188
+ // while it waits: the badge carries its ×, shown once the bubble has its
189
+ // enqueued_id (style.css; an echo before its ack has none) and handled by
190
+ // unqueue.js. Not a next-step line: a step boundary may merge it any moment.
187
191
  export function stateBadgeTemplate(state, delivery = null) {
188
192
  const text = badgeText(state, delivery);
189
- return text ? html`<span class="state-badge">${text}</span>` : nothing;
193
+ if (!text) return nothing;
194
+ const remove = state === "queued" && delivery === "queue"
195
+ ? html`<button type="button" class="unqueue" title="Remove from queue" aria-label="Remove from queue"></button>`
196
+ : nothing;
197
+ return html`<span class="state-badge">${text}${remove}</span>`;
190
198
  }
191
199
 
192
200
  // A `!cmd`'s saved user message (the worker appends "!(<command>)\n<output>"
@@ -0,0 +1,56 @@
1
+ // The × on a message queued for after the running turn (its badge,
2
+ // turn_html.js stateBadgeTemplate): POST …/turn/:enqueued_id/dequeue
3
+ // (Bridge#handle_dequeue_turn) takes it back before it runs. The bubble
4
+ // goes with the turn_dequeued every tab gets (bubbles.js op "remove"), and
5
+ // here at the 200 already. Its text comes back (edit): into an empty
6
+ // composer, to change and send again; over a draft only in a toast (the
7
+ // draft stays). Its images don't come back. A refusal keeps it and says why
8
+ // in a toast. No confirm: nothing is lost. Pure but for the
9
+ // injected calls, so `node --test` covers it; app.js binds onClick to the
10
+ // turn roots.
11
+ import { dequeueTurn as postDequeue } from "./data.js";
12
+ import { removeQueuedBubble } from "./bubbles.js";
13
+
14
+ // Why a removal didn't happen, in words.
15
+ export function unqueueFailureText(error) {
16
+ if (error?.code === "already_started") return "Not removed: it already started";
17
+ if (error?.code === "not_queued") return "Not removed: it goes in at the running turn's next step";
18
+ if (error?.status === 404 && error?.code === "unknown_enqueued_id") return "Not removed: it is already gone";
19
+ return `Not removed: ${error?.message || "the worker did not answer"}`;
20
+ }
21
+
22
+ // The removed text in a toast, clipped.
23
+ const SHOWN = 200;
24
+ function clipped(text) {
25
+ const line = String(text || "").replace(/\s+/g, " ").trim();
26
+ return line.length > SHOWN ? `${line.slice(0, SHOWN)}\u2026` : line;
27
+ }
28
+
29
+ // +sessionId+ is read at each click (the open session); +clientId+ is this
30
+ // tab's (the turn_dequeued names who removed it); +putBack(text)+ fills the
31
+ // composer when it is empty and says whether it did.
32
+ export function createUnqueue({ sessionId, clientId = null, dequeue = postDequeue, toast = () => {}, putBack = () => false }) {
33
+ // A click on a queued bubble's ×; any other click is left alone.
34
+ async function onClick(e) {
35
+ const button = e.target?.closest?.("button.unqueue");
36
+ if (!button || button.disabled) return null;
37
+ const bubble = button.closest(".bubble.user");
38
+ const enqueuedId = bubble?.dataset.enqueuedId;
39
+ const id = sessionId();
40
+ if (!enqueuedId || !id) return null;
41
+ e.stopPropagation?.();
42
+ button.disabled = true;
43
+ try {
44
+ const result = await dequeue(id, enqueuedId, { clientId });
45
+ removeQueuedBubble(bubble);
46
+ const text = String(result?.prompt || "");
47
+ if (text.trim() && !putBack(text)) toast(`Removed (your draft stays): ${clipped(text)}`);
48
+ return result;
49
+ } catch (error) {
50
+ if (button.isConnected) button.disabled = false;
51
+ toast(unqueueFailureText(error));
52
+ return null;
53
+ }
54
+ }
55
+ return { onClick };
56
+ }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: samagotchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.83.0
4
+ version: 0.85.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -180,6 +180,7 @@ files:
180
180
  - lib/samagotchi/broadcast_command.rb
181
181
  - lib/samagotchi/bundle_command.rb
182
182
  - lib/samagotchi/bundle_needs.rb
183
+ - lib/samagotchi/bundle_session_settings.rb
183
184
  - lib/samagotchi/bundles/agents-md/manifest.yml
184
185
  - lib/samagotchi/bundles/agents-md/plugin.rb
185
186
  - lib/samagotchi/bundles/btw/manifest.yml
@@ -690,6 +691,7 @@ files:
690
691
  - lib/samagotchi/web/public/turn_html.js
691
692
  - lib/samagotchi/web/public/turn_model.js
692
693
  - lib/samagotchi/web/public/turn_view.js
694
+ - lib/samagotchi/web/public/unqueue.js
693
695
  - lib/samagotchi/web/public/update_notice.js
694
696
  - lib/samagotchi/web/public/vendor/directive-helpers.js
695
697
  - lib/samagotchi/web/public/vendor/directive.js