tide-commander 1.136.0 → 1.136.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{BossLogsModal-B5wtQ9Fh.js → BossLogsModal-B4p16n9B.js} +1 -1
- package/dist/assets/{BossSpawnModal-B8twkNNt.js → BossSpawnModal-BHQ7sxeu.js} +1 -1
- package/dist/assets/{ControlsModal-BKUVlO45.js → ControlsModal-BcP5-m0h.js} +1 -1
- package/dist/assets/{DockerLogsModal-Oa0WYexL.js → DockerLogsModal-C_Gd6YLn.js} +1 -1
- package/dist/assets/{EmbeddedEditor-Cnkfr9vJ.js → EmbeddedEditor-GHVeXIn5.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-Cfsgqs5D.js → GmailOAuthSetup-VjAKcJf9.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-BZCY7oIc.js → GoogleOAuthSetup-DFbaN0Ry.js} +1 -1
- package/dist/assets/{IframeModal-BVaOFzOL.js → IframeModal-BaoVOZTf.js} +1 -1
- package/dist/assets/{IntegrationsPanel-CdcwTSOY.js → IntegrationsPanel-Cg4TxOfh.js} +2 -2
- package/dist/assets/{LogViewerModal-U0o-ixDu.js → LogViewerModal-CN2GYpgr.js} +1 -1
- package/dist/assets/{MonitoringModal-CdYBNEBK.js → MonitoringModal-oN_JjoC2.js} +1 -1
- package/dist/assets/{PM2LogsModal-cxSSfOTz.js → PM2LogsModal-BOQ1Im-i.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-BkzbecLN.js → RestoreArchivedAreaModal-DYcjqjbA.js} +1 -1
- package/dist/assets/{Scene2DCanvas-GGc__FVu.js → Scene2DCanvas-CsBA5ERQ.js} +1 -1
- package/dist/assets/{SceneManager-BWyie0tH.js → SceneManager-BqVfBAQc.js} +1 -1
- package/dist/assets/{SkillsPanel-DS7QNt0-.js → SkillsPanel-CtfixcgQ.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-CjHaygiR.js → SlackMultiInstanceSetup-3nGO_xs6.js} +1 -1
- package/dist/assets/{SpawnModal-MWySBWkA.js → SpawnModal-BvUHN-Q1.js} +1 -1
- package/dist/assets/{StatisticsModal-D08htYhH.js → StatisticsModal-DIx8l3F0.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-DULM1xpx.js → SubordinateAssignmentModal-Bw-V4i4-.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-nB6R7Nk4.js → TriggerManagerPanel-C2Mz8wVK.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-CKgW8DhB.js → WorkflowEditorPanel-C2RHV8Dx.js} +1 -1
- package/dist/assets/{area-logos-CuU1qdkU.js → area-logos-aaW_jqzI.js} +1 -1
- package/dist/assets/{index-CLmNQzTF.js → index-B4JG--zt.js} +2 -2
- package/dist/assets/{index-CQPpjjTd.js → index-BGz62M0g.js} +1 -1
- package/dist/assets/{index-Cc_Ys4_9.js → index-BIUEhCXu.js} +1 -1
- package/dist/assets/{index-ih8yuWhL.js → index-BT7u2ftY.js} +1 -1
- package/dist/assets/{index-CWYcGGCJ.js → index-BvR0it5l.js} +5 -5
- package/dist/assets/{index-8DzLSviG.js → index-BzCUPOjQ.js} +1 -1
- package/dist/assets/{index-B6TRH1E2.js → index-CmGwjdPh.js} +1 -1
- package/dist/assets/{index-CWiGs6nn.js → index-D6r1xAEg.js} +1 -1
- package/dist/assets/{index-Csnk6THj.js → index-DidVGiaJ.js} +2 -2
- package/dist/assets/main-Ba3wdQK_.css +1 -0
- package/dist/assets/{main-CHE4wC7P.js → main-rDQDtv_U.js} +115 -115
- package/dist/assets/{web-C4yrL2Hp.js → web-B5Ma_-eu.js} +1 -1
- package/dist/assets/{web-D1Ur_Mtd.js → web-DojkIxYY.js} +1 -1
- package/dist/assets/{web-dmnfXY_q.js → web-Dy1328BB.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/src/packages/server/claude/backend.js +56 -6
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +4 -0
- package/dist/src/packages/server/claude/runner/watchdog.js +34 -0
- package/dist/src/packages/server/routes/files.js +297 -92
- package/dist/src/packages/server/services/runtime-events.js +76 -9
- package/dist/src/packages/server/services/runtime-subagents.js +42 -0
- package/package.json +1 -1
- package/dist/assets/main-DM0iS4EN.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cO as a}from"./main-
|
|
1
|
+
import{cO as a}from"./main-rDQDtv_U.js";import{ImpactStyle as i,NotificationType as r}from"./index-B4JG--zt.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cO as t}from"./main-
|
|
1
|
+
import{cO as t}from"./main-rDQDtv_U.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cO as s}from"./main-
|
|
1
|
+
import{cO as s}from"./main-rDQDtv_U.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
|
package/dist/index.html
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
|
23
23
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
|
|
24
24
|
<title>Tide Commander</title>
|
|
25
|
-
<script type="module" crossorigin src="/assets/main-
|
|
25
|
+
<script type="module" crossorigin src="/assets/main-rDQDtv_U.js"></script>
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
|
|
28
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
28
|
+
<link rel="stylesheet" crossorigin href="/assets/main-Ba3wdQK_.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -15,6 +15,12 @@ const log = createLogger('Backend');
|
|
|
15
15
|
// Track tool_use_id to tool_name mapping for matching tool_result events
|
|
16
16
|
// This is a module-level map that persists across parseEvent calls
|
|
17
17
|
const toolUseIdToName = new Map();
|
|
18
|
+
// Marker text of the immediate tool_result the CLI returns when a Task/Agent tool
|
|
19
|
+
// launches in the background (the real result arrives later as a <task-notification>)
|
|
20
|
+
function isBackgroundLaunchStub(content) {
|
|
21
|
+
return content.includes('Async agent launched successfully')
|
|
22
|
+
|| content.includes('The agent is working in the background');
|
|
23
|
+
}
|
|
18
24
|
/**
|
|
19
25
|
* Write prompt content to a temp file for use with --system-prompt-file / --append-system-prompt-file
|
|
20
26
|
* This avoids issues with multiline prompts and shell escaping
|
|
@@ -268,7 +274,7 @@ export class ClaudeBackend {
|
|
|
268
274
|
const message = event.message;
|
|
269
275
|
// Handle array content (tool_result blocks)
|
|
270
276
|
if (Array.isArray(message?.content)) {
|
|
271
|
-
const
|
|
277
|
+
const events = [];
|
|
272
278
|
for (const block of message.content) {
|
|
273
279
|
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
274
280
|
// Prefer tool_use_result.stdout (raw output) over block.content (may be truncated)
|
|
@@ -288,6 +294,16 @@ export class ClaudeBackend {
|
|
|
288
294
|
// Look up the tool name from the tool_use_id mapping
|
|
289
295
|
const toolName = toolUseIdToName.get(block.tool_use_id) || 'unknown';
|
|
290
296
|
log.log(`parseUserEvent: Found tool_result for tool_use_id=${block.tool_use_id}, toolName=${toolName}, content length=${content?.length || 0}, hasToolUseResult=${!!event.tool_use_result}`);
|
|
297
|
+
// Background Task launch stub: the CLI answers the tool_use immediately with
|
|
298
|
+
// "Async agent launched..." while the subagent keeps running. Reclassify as
|
|
299
|
+
// task_started so downstream doesn't complete the subagent or idle the agent —
|
|
300
|
+
// the real completion arrives later as a <task-notification> user message.
|
|
301
|
+
if ((toolName === 'Task' || toolName === 'Agent') && isBackgroundLaunchStub(content)) {
|
|
302
|
+
log.log(`parseUserEvent: background launch stub for tool_use_id=${block.tool_use_id} — task still running`);
|
|
303
|
+
events.push({ type: 'task_started', toolUseId: block.tool_use_id });
|
|
304
|
+
toolUseIdToName.delete(block.tool_use_id);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
291
307
|
const toolResult = {
|
|
292
308
|
type: 'tool_result',
|
|
293
309
|
toolName,
|
|
@@ -310,14 +326,29 @@ export class ClaudeBackend {
|
|
|
310
326
|
if (event.parent_tool_use_id) {
|
|
311
327
|
toolResult.parentToolUseId = event.parent_tool_use_id;
|
|
312
328
|
}
|
|
313
|
-
|
|
329
|
+
events.push(toolResult);
|
|
314
330
|
// Clean up the mapping after use (tool_use_id is unique per invocation)
|
|
315
331
|
toolUseIdToName.delete(block.tool_use_id);
|
|
316
332
|
}
|
|
317
333
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
334
|
+
// Background task completions: the CLI wakes the model with a user message
|
|
335
|
+
// containing <task-notification> blocks (one per finished background task).
|
|
336
|
+
for (const block of message.content) {
|
|
337
|
+
if (block.type !== 'text' || typeof block.text !== 'string' || !block.text.includes('<task-notification>')) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
for (const chunk of block.text.match(/<task-notification>[\s\S]*?(?:<\/task-notification>|$)/g) ?? []) {
|
|
341
|
+
const toolUseId = /<tool-use-id>([^<]+)<\/tool-use-id>/.exec(chunk)?.[1];
|
|
342
|
+
const taskId = /<task-id>([^<]+)<\/task-id>/.exec(chunk)?.[1];
|
|
343
|
+
if (toolUseId) {
|
|
344
|
+
log.log(`parseUserEvent: task_notification for tool_use_id=${toolUseId} (task_id=${taskId || 'unknown'})`);
|
|
345
|
+
events.push({ type: 'task_notification', toolUseId, taskId });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (events.length > 0) {
|
|
350
|
+
log.log(`parseUserEvent: Extracted ${events.length} event(s)`);
|
|
351
|
+
return events.length === 1 ? events[0] : events;
|
|
321
352
|
}
|
|
322
353
|
}
|
|
323
354
|
// Check for local-command-stdout (from /context, /cost, /usage etc. commands)
|
|
@@ -354,9 +385,28 @@ export class ClaudeBackend {
|
|
|
354
385
|
errorMessage: event.error,
|
|
355
386
|
};
|
|
356
387
|
}
|
|
357
|
-
//
|
|
388
|
+
// Background Task launched (links task_id to the Task tool_use_id). The tool_result
|
|
389
|
+
// that follows immediately is only a launch stub — the task keeps running until a
|
|
390
|
+
// <task-notification> user message arrives.
|
|
358
391
|
if (event.subtype === 'task_started' && event.task_id) {
|
|
359
392
|
log.log(`parseSystemEvent: task_started - task_id=${event.task_id}, tool_use_id=${event.tool_use_id}`);
|
|
393
|
+
if (event.tool_use_id) {
|
|
394
|
+
return {
|
|
395
|
+
type: 'task_started',
|
|
396
|
+
taskId: event.task_id,
|
|
397
|
+
toolUseId: event.tool_use_id,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Background task finished (fires for async Task/Agent launches and for slow Bash
|
|
402
|
+
// commands the CLI promotes to tasks) — resolves pending-task tracking.
|
|
403
|
+
if (event.subtype === 'task_notification' && (event.tool_use_id || event.task_id)) {
|
|
404
|
+
log.log(`parseSystemEvent: task_notification - task_id=${event.task_id}, tool_use_id=${event.tool_use_id}`);
|
|
405
|
+
return {
|
|
406
|
+
type: 'task_notification',
|
|
407
|
+
taskId: event.task_id,
|
|
408
|
+
toolUseId: event.tool_use_id,
|
|
409
|
+
};
|
|
360
410
|
}
|
|
361
411
|
// Context compaction status
|
|
362
412
|
if (event.subtype === 'status' && event.status === 'compacting') {
|
|
@@ -85,6 +85,10 @@ export class RunnerStdoutPipeline {
|
|
|
85
85
|
}
|
|
86
86
|
const eventOrEvents = this.backend.parseEvent(rawEvent);
|
|
87
87
|
if (!eventOrEvents) {
|
|
88
|
+
// Even unmapped events (task_progress, thinking_tokens, ...) prove the CLI is
|
|
89
|
+
// alive and mid-work — keep the watchdog's activity clock fresh so a session
|
|
90
|
+
// waiting on background tasks isn't idle-killed or flagged as stuck.
|
|
91
|
+
this.bus.emit({ type: 'runner.activity', agentId, timestamp: Date.now() });
|
|
88
92
|
return;
|
|
89
93
|
}
|
|
90
94
|
if (Array.isArray(eventOrEvents)) {
|
|
@@ -2,6 +2,7 @@ import { createLogger } from '../../utils/logger.js';
|
|
|
2
2
|
import { isProcessRunning } from '../../data/index.js';
|
|
3
3
|
import { hasTmuxSession, isTmuxPaneCommandAlive, killTmuxSession } from './tmux-helper.js';
|
|
4
4
|
import * as agentService from '../../services/agent-service.js';
|
|
5
|
+
import { clearPendingBackgroundTasks } from '../../services/runtime-subagents.js';
|
|
5
6
|
import { getTmuxIdleTimeoutMs } from '../../services/system-prompt-service.js';
|
|
6
7
|
const log = createLogger('Runner');
|
|
7
8
|
const MAX_DEATH_HISTORY = 50;
|
|
@@ -10,6 +11,13 @@ const MAX_DEATH_HISTORY = 50;
|
|
|
10
11
|
// can hang for many minutes without an exit signal) and SIGKILL it so the normal
|
|
11
12
|
// process_closed → maybeAutoRestart path can respawn with session resume.
|
|
12
13
|
const IDLE_RESPAWN_MS = Number.parseInt(process.env.TIDE_IDLE_RESPAWN_MS ?? '', 10) || 180_000;
|
|
14
|
+
// A 'working' agent whose turn already ended (waiting_for_input) and whose stream has
|
|
15
|
+
// been silent this long is stuck — a background-task completion signal we missed
|
|
16
|
+
// (killed task, CLI wording change) or a stale status restored across a server
|
|
17
|
+
// restart. Reconcile to idle. Live background tasks emit task_progress lines that
|
|
18
|
+
// count as activity, and message sends flip turnState to 'processing', so neither
|
|
19
|
+
// can trip this.
|
|
20
|
+
const STUCK_WORKING_RECONCILE_MS = Number.parseInt(process.env.TIDE_STUCK_WORKING_RECONCILE_MS ?? '', 10) || 180_000;
|
|
13
21
|
export class RunnerWatchdog {
|
|
14
22
|
activeProcesses;
|
|
15
23
|
lastStderr;
|
|
@@ -28,6 +36,7 @@ export class RunnerWatchdog {
|
|
|
28
36
|
const idleTimeoutMs = getTmuxIdleTimeoutMs();
|
|
29
37
|
const now = Date.now();
|
|
30
38
|
for (const [agentId, activeProcess] of this.activeProcesses) {
|
|
39
|
+
this.reconcileStuckWorking(agentId, activeProcess, now);
|
|
31
40
|
// tmux mode: the launcher PID exits immediately — check the tmux session instead
|
|
32
41
|
if (activeProcess.tmuxSession) {
|
|
33
42
|
const sessionAlive = hasTmuxSession(agentId);
|
|
@@ -146,6 +155,31 @@ export class RunnerWatchdog {
|
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
}
|
|
158
|
+
// Safety net for stuck 'working' status: the turn ended, no events are flowing,
|
|
159
|
+
// yet the agent still shows working. Flip it to idle and drop any pending
|
|
160
|
+
// background-task tracking so the tmux idle-timeout can later reclaim the session.
|
|
161
|
+
reconcileStuckWorking(agentId, activeProcess, now) {
|
|
162
|
+
if (activeProcess.turnState !== 'waiting_for_input') {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const lastActivity = activeProcess.lastActivityTime ?? activeProcess.startTime;
|
|
166
|
+
if (now - lastActivity < STUCK_WORKING_RECONCILE_MS) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const agent = agentService.getAgent(agentId);
|
|
170
|
+
if (agent?.status !== 'working') {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const silentSec = Math.round((now - lastActivity) / 1000);
|
|
174
|
+
log.error(`🐕 [WATCHDOG] Agent ${agentId}: status 'working' but turn ended and stream silent for ${silentSec}s `
|
|
175
|
+
+ `(threshold=${STUCK_WORKING_RECONCILE_MS / 1000}s) — reconciling to idle and clearing pending background tasks`);
|
|
176
|
+
clearPendingBackgroundTasks(agentId);
|
|
177
|
+
agentService.updateAgent(agentId, {
|
|
178
|
+
status: 'idle',
|
|
179
|
+
currentTask: undefined,
|
|
180
|
+
currentTool: undefined,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
149
183
|
recordDeath(info) {
|
|
150
184
|
this.recentDeaths.unshift(info);
|
|
151
185
|
if (this.recentDeaths.length > MAX_DEATH_HISTORY) {
|