Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
822 changes: 822 additions & 0 deletions dist/state-store.js

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions scripts/claudeman-launchd-wrapper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash
# Claudeman launchd wrapper
#
# 根因: Node 25 被 launchd 直接拉起时 V8 bootstrapper 概率性死锁
# (进程存在、端口不监听、日志空白、sample 显示卡在 LoadEnvironment)
# 手动 nohup 同样环境则正常。通过 bash wrapper + exec 绕过此问题。
#
# 额外加固:
# - 启动前清理占 3000 端口的野进程
# - 写启动日志到 stderr(被 launchd 重定向到 StandardErrorPath)

set -euo pipefail

PORT=3000
CLAUDEMAN_DIR="/Users/teigen/Documents/Workspace/AI_project/Claudeman"

export HOME=/Users/teigen
export PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin

echo "[wrapper] $(date '+%Y-%m-%d %H:%M:%S') starting claudeman web" >&2

# 清理占端口的野进程(非本进程树的残留 node)
STALE_PIDS=$(/usr/sbin/lsof -nP -iTCP:${PORT} -sTCP:LISTEN -t 2>/dev/null || true)
if [[ -n "$STALE_PIDS" ]]; then
echo "[wrapper] clearing stale processes on port ${PORT}: ${STALE_PIDS}" >&2
for pid in $STALE_PIDS; do
kill "$pid" 2>/dev/null || true
done
sleep 2
fi

cd "$CLAUDEMAN_DIR"
exec /opt/homebrew/bin/node dist/index.js web --https -p "$PORT"
4 changes: 2 additions & 2 deletions src/state-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ export class StateStore {

this.ensureDir();

const tempPath = this.filePath + '.tmp';
const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
const backupPath = this.filePath + '.bak';

// Step 1: Serialize state (validates it's JSON-safe)
Expand Down Expand Up @@ -373,7 +373,7 @@ export class StateStore {

this.ensureDir();

const tempPath = this.filePath + '.tmp';
const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
const backupPath = this.filePath + '.bak';

const json = this.serializeState();
Expand Down
6 changes: 6 additions & 0 deletions src/web/public/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,12 @@ const SSE_EVENTS = {
ORCHESTRATOR_TASK_FAILED: 'orchestrator:taskFailed',
ORCHESTRATOR_COMPLETED: 'orchestrator:completed',
ORCHESTRATOR_ERROR: 'orchestrator:error',

// Cases
CASE_CREATED: 'case:created',
CASE_LINKED: 'case:linked',
CASE_DELETED: 'case:deleted',
CASE_ORDER_CHANGED: 'case:order-changed',
};

// ═══════════════════════════════════════════════════════════════
Expand Down
8 changes: 8 additions & 0 deletions src/web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,7 @@ <h3>Add Case</h3>
<div class="modal-tabs">
<button class="modal-tab-btn active" data-tab="case-create">Create New</button>
<button class="modal-tab-btn" data-tab="case-link">Link Existing</button>
<button class="modal-tab-btn" data-tab="case-manage">Manage</button>
</div>
<div class="modal-body">
<!-- Create New Tab -->
Expand Down Expand Up @@ -1387,6 +1388,13 @@ <h3>Add Case</h3>
<span class="form-hint">Absolute path to an existing project folder, e.g. /home/you/my-project</span>
</div>
</div>
<!-- Manage Tab -->
<div class="modal-tab-content hidden" id="case-manage">
<div class="case-manage-list" id="caseManageList">
<!-- Populated by JS -->
</div>
<span class="form-hint" style="margin-top: 8px; display: block;">Drag or use arrows to reorder. Changes are saved automatically.</span>
</div>
</div>
<div class="form-actions">
<button class="btn-toolbar" onclick="app.closeCreateCaseModal()">Cancel</button>
Expand Down
50 changes: 43 additions & 7 deletions src/web/public/keyboard-accessory.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Defines two exports:
*
* - KeyboardAccessoryBar (singleton object) — Quick action buttons shown above the virtual
* keyboard on mobile: arrow up/down, /init, /clear, /compact, paste, and dismiss.
* keyboard on mobile: Esc, arrow up/down, Tab, Shift+Tab, Ctrl+O, /init, /clear, /compact, paste, and dismiss.
* Destructive actions (/clear, /compact) require double-tap confirmation (2s amber state).
* Commands are sent as text + Enter separately for Ink compatibility.
* Only initializes on touch devices (MobileDetection.isTouchDevice guard).
Expand Down Expand Up @@ -53,17 +53,32 @@ const KeyboardAccessoryBar = {
<path d="M19 9l-7 7-7-7"/>
</svg>
</button>
<button class="accessory-btn" data-action="init" title="/init">/init</button>
<button class="accessory-btn" data-action="clear" title="/clear">/clear</button>
<button class="accessory-btn" data-action="compact" title="/compact">/compact</button>
<button class="accessory-btn accessory-btn-arrow" data-action="arrow-left" title="Arrow left">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M15 19l-7-7 7-7"/>
</svg>
</button>
<button class="accessory-btn accessory-btn-arrow" data-action="arrow-right" title="Arrow right">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M9 5l7 7-7 7"/>
</svg>
</button>
<button class="accessory-btn" data-action="paste" title="Paste from clipboard">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
</svg>
</button>
<button class="accessory-btn accessory-btn-dismiss" data-action="dismiss" title="Dismiss keyboard">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
<button class="accessory-btn" data-action="tab" title="Tab">Tab</button>
<button class="accessory-btn" data-action="shift-tab" title="Shift+Tab">⇧Tab</button>
<button class="accessory-btn" data-action="ctrl-o" title="Ctrl+O">⌃O</button>
<button class="accessory-btn" data-action="opt-enter" title="Option+Enter (newline)">⌥Enter</button>
<button class="accessory-btn" data-action="esc" title="Escape">Esc</button>
<button class="accessory-btn" data-action="init" title="/init">/init</button>
<button class="accessory-btn" data-action="clear" title="/clear">/clear</button>
<button class="accessory-btn" data-action="compact" title="/compact">/compact</button>
<button class="accessory-btn accessory-btn-arrow" data-action="dismiss" title="Dismiss keyboard">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M19 9l-7 7-7-7"/>
</svg>
</button>
Expand All @@ -80,7 +95,7 @@ const KeyboardAccessoryBar = {
this.handleAction(action, btn);

// Refocus terminal so keyboard stays open (tap blurs terminal → keyboard dismisses → toolbar shifts)
if ((action === 'scroll-up' || action === 'scroll-down') ||
if ((action === 'scroll-up' || action === 'scroll-down' || action === 'arrow-left' || action === 'arrow-right' || action === 'tab' || action === 'shift-tab' || action === 'ctrl-o' || action === 'opt-enter' || action === 'esc') ||
((action === 'clear' || action === 'compact') && this._confirmAction)) {
if (typeof app !== 'undefined' && app.terminal) {
app.terminal.focus();
Expand Down Expand Up @@ -109,6 +124,27 @@ const KeyboardAccessoryBar = {
case 'scroll-down':
this.sendKey('\x1b[B');
break;
case 'arrow-left':
this.sendKey('\x1b[D');
break;
case 'arrow-right':
this.sendKey('\x1b[C');
break;
case 'esc':
this.sendKey('\x1b');
break;
case 'opt-enter':
this.sendKey('\x1b\r');
break;
case 'tab':
this.sendKey('\t');
break;
case 'shift-tab':
this.sendKey('\x1b[Z');
break;
case 'ctrl-o':
this.sendKey('\x0f');
break;
case 'init':
this.sendCommand('/init');
break;
Expand Down
53 changes: 15 additions & 38 deletions src/web/public/mobile.css
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,8 @@ html.mobile-init .file-browser-panel {
padding-right: calc(8px + var(--safe-area-right));
gap: 8px;
align-items: center;
justify-content: center;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
z-index: 51;
transition: transform 0.15s ease-out;
will-change: transform;
Expand All @@ -896,10 +897,16 @@ html.mobile-init .file-browser-panel {
display: flex;
}

/* 隐藏滚动条但保留滑动能力 */
.keyboard-accessory-bar::-webkit-scrollbar {
display: none;
}

.accessory-btn {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
gap: 4px;
padding: 6px 12px;
background: #2a2a2a;
Expand Down Expand Up @@ -939,24 +946,6 @@ html.mobile-init .file-browser-panel {
background: #2563eb;
}

.accessory-btn-dismiss {
padding: 8px 14px;
background: #2a2a2a;
border: 1.5px solid rgba(255, 255, 255, 0.25);
border-radius: 6px;
color: #e5e5e5;
}

.accessory-btn-dismiss svg {
width: 22px;
height: 22px;
stroke-width: 3;
}

.accessory-btn-dismiss:active {
background: #3a3a3a;
}

/* Voice preview — positioned above accessory bar on mobile */
.voice-preview {
bottom: calc(var(--safe-area-bottom) + 94px);
Expand Down Expand Up @@ -2068,18 +2057,24 @@ html.mobile-init .file-browser-panel {
padding: 6px 8px;
gap: 8px;
align-items: center;
justify-content: center;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
z-index: 51;
}

.keyboard-accessory-bar.visible {
display: flex;
}

.keyboard-accessory-bar::-webkit-scrollbar {
display: none;
}

.accessory-btn {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
gap: 4px;
padding: 6px 12px;
background: #2a2a2a;
Expand Down Expand Up @@ -2118,24 +2113,6 @@ html.mobile-init .file-browser-panel {
background: #2563eb;
}

.accessory-btn-dismiss {
padding: 8px 14px;
background: #2a2a2a;
border: 1.5px solid rgba(255, 255, 255, 0.25);
border-radius: 6px;
color: #e5e5e5;
}

.accessory-btn-dismiss svg {
width: 22px;
height: 22px;
stroke-width: 3;
}

.accessory-btn-dismiss:active {
background: #3a3a3a;
}

/* ============================================================================
iOS Safari Specific Fixes
============================================================================ */
Expand Down
Loading