claude-pager 0.3.14 → 0.3.15

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.
@@ -15,6 +15,7 @@ export interface DashboardSession {
15
15
  context?: string;
16
16
  agoSeconds: number;
17
17
  };
18
+ lastAssistantText?: string;
18
19
  git: GitInfo;
19
20
  needsTesting: boolean;
20
21
  committed: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"enricher.d.ts","sourceRoot":"","sources":["../../src/dashboard/enricher.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAkB9C,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,GAAG,eAAe,GAAG,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAAC;IAC/E,eAAe,CAAC,EAAE;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,GAAG,EAAE,OAAO,CAAC;IACb,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,EAAE,CAAC,EAAE,eAAe,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAuJnE"}
1
+ {"version":3,"file":"enricher.d.ts","sourceRoot":"","sources":["../../src/dashboard/enricher.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAkB9C,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,GAAG,eAAe,GAAG,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAAC;IAC/E,eAAe,CAAC,EAAE;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IAMF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,GAAG,EAAE,OAAO,CAAC;IACb,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,EAAE,CAAC,EAAE,eAAe,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAgKnE"}
@@ -93,6 +93,13 @@ async function getDashboardData() {
93
93
  agoSeconds: Math.floor((Date.now() - pendingQ.notifiedAt) / 1000),
94
94
  };
95
95
  })() : undefined,
96
+ // Surface the latest assistant text when the session is not
97
+ // actively working AND there is no pending event to render. This
98
+ // covers the gap between Claude finishing a turn and Claude Code
99
+ // firing its idle_prompt notification 60s later.
100
+ lastAssistantText: (!pendingQ
101
+ && state !== 'working'
102
+ && transcript.lastAssistantText) ? transcript.lastAssistantText.slice(-3000) : undefined,
96
103
  git,
97
104
  needsTesting: false, // computed at project level after CI fetch
98
105
  committed: git.modifiedFiles === 0 || transcript.recentCommit,
@@ -1 +1 @@
1
- {"version":3,"file":"enricher.js","sourceRoot":"","sources":["../../src/dashboard/enricher.ts"],"names":[],"mappings":";;AAgEA,4CAuJC;AAvND,2DAAkD;AAClD,uDAAyE;AACzE,uDAA6D;AAC7D,qDAAmE;AACnE,gDAA8C;AAC9C,iDAAgD;AAChD,mDAAqD;AACrD,mDAA+C;AAC/C,qDAAkD;AAKlD,gEAAgE;AAChE,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEhD,SAAS,eAAe,CAAC,QAAgB,EAAE,KAAa;IACtD,2CAA2C;IAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IACpE,IAAI,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,KAAK;QAAE,OAAO;IAElD,IAAI,CAAC;QACH,IAAA,iCAAY,EAAC,MAAM,EAAE,CAAC,eAAe,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAClF,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,uCAAuC;IACzC,CAAC;AACH,CAAC;AAqCM,KAAK,UAAU,gBAAgB;IACpC,IAAA,8BAAiB,GAAE,CAAC;IACpB,MAAM,QAAQ,GAAG,IAAA,yBAAY,GAAE,CAAC;IAChC,MAAM,OAAO,GAAG,IAAA,uBAAW,GAAE,CAAC;IAC9B,MAAM,MAAM,GAAG,IAAA,qBAAU,GAAE,CAAC;IAE5B,MAAM,MAAM,GAAG,EAAE,GAAG,QAAQ,CAAC;IAE7B,MAAM,QAAQ,GAAG,CAAC,MAAM,OAAO,CAAC,GAAG,CACjC,QAAQ;SACL,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,IAAA,gCAAmB,EAAC,CAAC,CAAC,CAAC;SACnC,GAAG,CAAC,KAAK,EAAE,OAAO,EAA+C,EAAE;QAClE,MAAM,UAAU,GAAG,IAAA,kCAAkB,EAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QACtE,MAAM,GAAG,GAAG,MAAM,IAAA,4BAAY,EAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5C,yFAAyF;QACzF,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;QACpF,IAAI,QAAQ,GACV,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,mBAAmB,CAAC;eAC3D,cAAc,CAAC,CAAC,CAAC,CAAC;QAEvB,iFAAiF;QACjF,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,OAAO;YACX,kFAAkF;YAClF,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,mBAAmB,IAAI,UAAU,CAAC,aAAa,GAAG,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAC;gBACtG,oEAAoE;gBACpE,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,aAAa,IAAI,UAAU,CAAC,KAAK,KAAK,SAAS,IAAI,UAAU,CAAC,aAAa,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;YAE9H,IAAI,OAAO,EAAE,CAAC;gBACZ,IAAA,yBAAa,EAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBACjC,QAAQ,GAAG,SAAS,CAAC;YACvB,CAAC;QACH,CAAC;QAED,qDAAqD;QACrD,IAAI,KAAK,GAA8B,UAAU,CAAC,KAAK,CAAC;QACxD,IAAI,QAAQ,EAAE,CAAC;YACb,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,mBAAmB;gBACjD,CAAC,CAAC,oBAAoB;gBACtB,CAAC,CAAC,eAAe,CAAC;QACtB,CAAC;QAED,gDAAgD;QAChD,IAAI,OAAO,CAAC,QAAQ,IAAI,UAAU,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,KAAK,eAAe,EAAE,CAAC;YACjF,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;YACvD,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,WAAW,KAAK,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC;QAC3E,CAAC;QAED,OAAO;YACL,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,KAAK,EAAE,UAAU,CAAC,KAAK;YACvB,KAAK;YACL,eAAe,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE;gBAChC,gEAAgE;gBAChE,iEAAiE;gBACjE,iEAAiE;gBACjE,6DAA6D;gBAC7D,8DAA8D;gBAC9D,uBAAuB;gBACvB,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,aAAa,CAAC;gBACrD,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,UAAU,CAAC,iBAAiB,CAAC;oBAClD,CAAC,CAAC,UAAU,CAAC,iBAAiB;oBAC9B,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC;gBAC3B,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,GAAG,IAAI;oBAC/B,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;oBAClD,CAAC,CAAC,GAAG,CAAC;gBACR,OAAO;oBACL,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE;oBAC1B,OAAO,EAAE,QAAQ,CAAC,OAAO;oBACzB,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI;oBACzB,OAAO;oBACP,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ;oBACjC,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS;oBACnC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,OAAO;oBAC/B,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;iBAClE,CAAC;YACJ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS;YAChB,GAAG;YACH,YAAY,EAAE,KAAK,EAAE,2CAA2C;YAChE,SAAS,EAAE,GAAG,CAAC,aAAa,KAAK,CAAC,IAAI,UAAU,CAAC,YAAY;YAC7D,MAAM,EAAE,GAAG,CAAC,eAAe,KAAK,CAAC,IAAI,UAAU,CAAC,UAAU;YAC1D,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE;YAChC,YAAY,EAAE,UAAU,CAAC,aAAa,IAAI,OAAO,CAAC,SAAS;YAC3D,GAAG,EAAE,OAAO,CAAC,GAAG;SACjB,CAAC;IACJ,CAAC,CAAC,CACL,CAAC;QACA,4FAA4F;SAC3F,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,eAAe,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,YAAY,GAAG,MAAM,CAAC,CAAC,CAAC;IAEvF,yBAAyB;IACzB,MAAM,UAAU,GAAG,IAAI,GAAG,EAAqD,CAAC;IAChF,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAC7C,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjB,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,UAAU,GAA2B;QACzC,kBAAkB,EAAE,CAAC;QACrB,aAAa,EAAE,CAAC;QAChB,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,CAAC;QACP,OAAO,EAAE,CAAC;KACX,CAAC;IAEF,uDAAuD;IACvD,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC;IACjD,MAAM,SAAS,GAAG,IAAI,GAAG,EAA2B,CAAC;IACrD,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;QACd,MAAM,UAAU,GAAG,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC9C,MAAM,SAAS,GAAG,MAAM,IAAA,+BAAc,EAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;YACvD,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAChC,CAAC,CAAC,CAAC;QACH,MAAM,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAChC,CAAC;IAED,MAAM,QAAQ,GAAuB,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;SAClE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,EAAE;QACxB,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC;QAE7B,sBAAsB;QACtB,4CAA4C;QAC5C,wEAAwE;QACxE,4EAA4E;QAC5E,MAAM,QAAQ,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,KAAK,QAAQ,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,KAAK,QAAQ,CAAC;QACnF,MAAM,SAAS,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,KAAK,SAAS,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC;QACtF,MAAM,WAAW,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAC1D,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;QAC1C,MAAM,YAAY,GAAG,QAAQ,IAAI,WAAW,IAAI,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAEhG,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC;QAClD,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,IAAI;YACJ,QAAQ,EAAE,QAAQ;iBACf,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;iBAC5D,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;YAC1E,KAAK,EAAE,IAAA,oBAAS,EAAC,WAAW,CAAC;YAC7B,EAAE;YACF,SAAS;SACV,CAAC;IACJ,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACb,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACxE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACxE,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IAEL,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;AAC7C,CAAC"}
1
+ {"version":3,"file":"enricher.js","sourceRoot":"","sources":["../../src/dashboard/enricher.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- export declare const DASHBOARD_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>claude-pager dashboard</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link href=\"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap\" rel=\"stylesheet\">\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n background: #0d1117;\n color: #c9d1d9;\n font-family: 'JetBrains Mono', monospace;\n min-height: 100vh;\n padding: 24px;\n }\n\n header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 32px;\n padding-bottom: 16px;\n border-bottom: 1px solid #21262d;\n }\n\n .logo {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n .logo h1 {\n font-size: 22px;\n font-weight: 700;\n color: #f0f6fc;\n }\n\n .cursor {\n display: inline-block;\n width: 10px;\n height: 20px;\n background: #58a6ff;\n animation: blink 1s step-end infinite;\n vertical-align: middle;\n margin-left: 4px;\n }\n\n @keyframes blink {\n 50% { opacity: 0; }\n }\n\n .status-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n display: inline-block;\n }\n\n .status-dot.connected { background: #3fb950; box-shadow: 0 0 6px #3fb950; }\n .status-dot.disconnected { background: #f85149; box-shadow: 0 0 6px #f85149; }\n\n .meta {\n font-size: 12px;\n color: #484f58;\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .project {\n margin-bottom: 28px;\n }\n\n .project-header {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 12px;\n }\n\n .project-header h2 {\n font-size: 16px;\n font-weight: 600;\n color: #58a6ff;\n }\n\n .project-count {\n background: #21262d;\n color: #8b949e;\n font-size: 11px;\n padding: 2px 8px;\n border-radius: 10px;\n }\n\n .project-path {\n font-size: 11px;\n color: #484f58;\n margin-left: auto;\n }\n\n .pin-btn {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 14px;\n opacity: 0.3;\n transition: opacity 0.2s;\n padding: 2px 4px;\n }\n\n .pin-btn:hover { opacity: 0.7; }\n .pin-btn.pinned { opacity: 1; }\n\n .dismiss-btn {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 12px;\n opacity: 0.25;\n transition: opacity 0.2s;\n padding: 2px 4px;\n }\n\n .dismiss-btn:hover { opacity: 0.8; color: #f85149; }\n\n .ci-row {\n display: flex;\n gap: 12px;\n margin-bottom: 12px;\n font-size: 11px;\n }\n\n .ci-badge {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 3px 10px;\n border-radius: 12px;\n font-weight: 600;\n text-decoration: none;\n transition: opacity 0.2s;\n }\n\n .ci-badge:hover { opacity: 0.8; }\n\n .ci-badge.success { background: #0d2818; color: #3fb950; }\n .ci-badge.failed { background: #490202; color: #f85149; }\n .ci-badge.running { background: #0d419d; color: #58a6ff; animation: pulse 2s ease-in-out infinite; }\n .ci-badge.pending { background: #3d2e00; color: #d29922; }\n .ci-badge.canceled { background: #21262d; color: #8b949e; }\n .ci-badge.unknown { background: #21262d; color: #484f58; }\n\n .ci-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n display: inline-block;\n }\n\n .ci-dot.success { background: #3fb950; }\n .ci-dot.failed { background: #f85149; }\n .ci-dot.running { background: #58a6ff; }\n .ci-dot.pending { background: #d29922; }\n .ci-dot.canceled { background: #8b949e; }\n .ci-dot.unknown { background: #484f58; }\n\n .sessions {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));\n gap: 12px;\n }\n\n .card {\n background: #161b22;\n border: 1px solid #21262d;\n border-radius: 8px;\n padding: 16px;\n transition: border-color 0.2s, box-shadow 0.2s, opacity 0.3s;\n }\n\n .card:hover {\n border-color: #388bfd44;\n box-shadow: 0 0 12px #388bfd22;\n }\n\n .card.stale {\n opacity: 0.45;\n border-style: dashed;\n }\n\n .card.stale:hover {\n opacity: 0.8;\n }\n\n .card.active {\n border-color: #388bfd44;\n border-left: 3px solid #58a6ff;\n }\n\n .card.alert {\n border-color: #f0883e44;\n border-left: 3px solid #f0883e;\n }\n\n .card-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n margin-bottom: 10px;\n }\n\n .card-title {\n font-size: 14px;\n font-weight: 600;\n color: #f0f6fc;\n line-height: 1.3;\n flex: 1;\n margin-right: 8px;\n overflow: hidden;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n }\n\n .card-title.expanded {\n -webkit-line-clamp: unset;\n overflow: visible;\n white-space: pre-wrap;\n }\n\n .expand-btn {\n background: none;\n border: none;\n color: #58a6ff;\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n cursor: pointer;\n padding: 2px 0;\n opacity: 0.8;\n }\n\n .expand-btn:hover { opacity: 1; }\n\n .badge {\n font-size: 10px;\n font-weight: 600;\n padding: 3px 8px;\n border-radius: 12px;\n white-space: nowrap;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .badge.working {\n background: #0d419d;\n color: #58a6ff;\n animation: pulse 2s ease-in-out infinite;\n }\n\n .badge.waiting_permission {\n background: #5a1e02;\n color: #f0883e;\n }\n\n .badge.waiting_input {\n background: #3d2e00;\n color: #d29922;\n }\n\n .badge.idle {\n background: #21262d;\n color: #8b949e;\n }\n\n .badge.unknown {\n background: #21262d;\n color: #484f58;\n }\n\n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.6; }\n }\n\n .pending-box {\n background: #1c1208;\n border: 1px solid #3d2e00;\n border-radius: 6px;\n padding: 8px 10px;\n margin-bottom: 10px;\n font-size: 12px;\n color: #d29922;\n }\n\n .pending-box .tool {\n color: #f0883e;\n font-weight: 600;\n }\n\n .pending-box .ago {\n color: #8b949e;\n float: right;\n }\n\n .action-row {\n display: flex;\n gap: 8px;\n margin-top: 8px;\n }\n\n .action-btn {\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n font-weight: 600;\n padding: 4px 14px;\n border-radius: 6px;\n border: none;\n cursor: pointer;\n transition: opacity 0.2s, transform 0.1s;\n }\n\n .action-btn:hover { opacity: 0.85; }\n .action-btn:active { transform: scale(0.96); }\n\n .action-btn.allow {\n background: #238636;\n color: #ffffff;\n }\n\n .action-btn.deny {\n background: #da3633;\n color: #ffffff;\n }\n\n .action-btn.allow-all {\n background: #1f6feb;\n color: #ffffff;\n margin-left: auto;\n }\n\n .action-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .reply-input {\n flex: 1;\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n padding: 4px 10px;\n border-radius: 6px;\n border: 1px solid #30363d;\n background: #0d1117;\n color: #c9d1d9;\n outline: none;\n }\n\n .reply-input:focus {\n border-color: #58a6ff;\n }\n\n .git-row {\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 11px;\n margin-bottom: 6px;\n }\n\n .git-branch {\n color: #8b949e;\n }\n\n .git-branch::before {\n content: '\u2387 ';\n }\n\n .git-modified {\n color: #f85149;\n }\n\n .git-unpushed {\n color: #d29922;\n }\n\n .git-clean {\n color: #3fb950;\n }\n\n .needs-testing {\n display: inline-block;\n font-size: 10px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n background: #490202;\n color: #f85149;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .flag {\n display: inline-block;\n font-size: 10px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n letter-spacing: 0.3px;\n }\n\n .flag.ok {\n background: #0d2818;\n color: #3fb950;\n }\n\n .flag.pending {\n background: #3d2e00;\n color: #d29922;\n }\n\n .card-footer {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 8px;\n font-size: 10px;\n color: #484f58;\n }\n\n .card-footer .spacer {\n margin-left: auto;\n }\n\n .notes-panel {\n background: #1a1e2e;\n border: 1px solid #2d333b;\n border-radius: 8px;\n padding: 12px;\n min-width: 0;\n }\n\n .notes-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 8px;\n font-size: 12px;\n font-weight: 600;\n color: #8b949e;\n }\n\n .notes-header .count {\n background: #2d333b;\n color: #c9d1d9;\n font-size: 10px;\n padding: 1px 7px;\n border-radius: 8px;\n }\n\n .note-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n border-radius: 4px;\n font-size: 12px;\n color: #c9d1d9;\n transition: background 0.15s;\n }\n\n .note-item:hover {\n background: #21262d;\n }\n\n .note-item .note-text {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: text;\n }\n\n .note-clock {\n font-size: 11px;\n cursor: default;\n flex-shrink: 0;\n }\n\n .note-thumb {\n width: 32px;\n height: 32px;\n object-fit: cover;\n border-radius: 4px;\n cursor: pointer;\n flex-shrink: 0;\n border: 1px solid #30363d;\n transition: transform 0.15s;\n }\n\n .note-thumb:hover {\n transform: scale(1.1);\n border-color: #58a6ff;\n }\n\n .note-lightbox {\n position: fixed;\n top: 0; left: 0; right: 0; bottom: 0;\n background: rgba(0,0,0,0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n cursor: pointer;\n }\n\n .note-lightbox img {\n max-width: 90vw;\n max-height: 90vh;\n border-radius: 8px;\n box-shadow: 0 0 40px rgba(0,0,0,0.5);\n }\n\n .note-grip {\n cursor: grab;\n color: #484f58;\n font-size: 10px;\n user-select: none;\n }\n .note-item.dragging {\n opacity: 0.4;\n }\n .note-item.drag-over {\n border-top: 2px solid #58a6ff;\n margin-top: -2px;\n }\n .note-btn.move {\n background: none;\n color: #484f58;\n padding: 0 2px;\n font-size: 8px;\n min-width: 16px;\n }\n .note-btn.move:hover {\n color: #58a6ff;\n }\n .note-btn {\n font-family: 'JetBrains Mono', monospace;\n font-size: 10px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 4px;\n border: none;\n cursor: pointer;\n transition: opacity 0.2s;\n }\n\n .note-btn:hover { opacity: 0.85; }\n\n .note-btn.send {\n background: #238636;\n color: #fff;\n }\n\n .note-btn.delete {\n background: #21262d;\n color: #8b949e;\n }\n\n .note-btn.delete:hover {\n background: #da3633;\n color: #fff;\n }\n\n .note-add-row {\n display: flex;\n gap: 6px;\n margin-top: 8px;\n }\n\n .empty {\n text-align: center;\n padding: 60px 20px;\n color: #484f58;\n }\n\n .empty h2 {\n font-size: 18px;\n color: #8b949e;\n margin-bottom: 8px;\n }\n\n .empty p {\n font-size: 13px;\n }\n\n @media (max-width: 768px) {\n body { padding: 12px; }\n\n header { flex-direction: column; align-items: flex-start; gap: 8px; }\n\n .logo h1 { font-size: 18px; }\n\n .sessions {\n grid-template-columns: 1fr;\n gap: 10px;\n }\n\n .project-header {\n flex-wrap: wrap;\n }\n\n .project-path { display: none; }\n\n .card { padding: 12px; }\n\n .card-title { font-size: 13px; }\n\n .action-btn {\n padding: 8px 18px;\n font-size: 13px;\n }\n\n .reply-input {\n font-size: 13px;\n padding: 8px 10px;\n }\n\n .ci-row { flex-wrap: wrap; gap: 6px; }\n\n .pending-box { font-size: 11px; }\n\n .pending-box code { font-size: 9px; }\n }\n\n @media (max-width: 480px) {\n body { padding: 8px; }\n\n .logo h1 { font-size: 16px; }\n\n .badge { font-size: 9px; padding: 2px 6px; }\n\n .git-row { flex-wrap: wrap; gap: 6px; }\n\n .action-btn {\n padding: 10px 20px;\n font-size: 14px;\n }\n\n .action-btn.allow-all {\n width: 100%;\n text-align: center;\n }\n }\n\n .scanline {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n background: repeating-linear-gradient(\n 0deg,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.03) 2px,\n rgba(0, 0, 0, 0.03) 4px\n );\n z-index: 999;\n }\n </style>\n</head>\n<body>\n <div class=\"scanline\"></div>\n <header>\n <div class=\"logo\">\n <h1>claude-pager<span class=\"cursor\"></span></h1>\n </div>\n <div class=\"meta\">\n <button class=\"action-btn allow-all\" id=\"allowAllBtn\" style=\"display:none\" onclick=\"allowAll()\">Allow All</button>\n <span class=\"status-dot connected\" id=\"statusDot\"></span>\n <span id=\"lastUpdate\">connecting...</span>\n </div>\n </header>\n <main id=\"projects\"></main>\n\n <script>\n let data = null;\n const sentMessages = new Map(); // sessionId \u2192 { icon, text, at }\n\n function getPinnedOrder() {\n try { return JSON.parse(localStorage.getItem('dashboard-pin-order') || '[]'); }\n catch { return []; }\n }\n\n function savePinnedOrder(order) {\n localStorage.setItem('dashboard-pin-order', JSON.stringify(order));\n }\n\n function togglePin(name) {\n const order = getPinnedOrder();\n const idx = order.indexOf(name);\n if (idx >= 0) {\n order.splice(idx, 1);\n } else {\n order.push(name);\n }\n savePinnedOrder(order);\n if (data) render(data);\n }\n\n function sortProjects(projects) {\n const pinned = getPinnedOrder();\n return [...projects].sort((a, b) => {\n const aPin = pinned.indexOf(a.name);\n const bPin = pinned.indexOf(b.name);\n const aIsPinned = aPin >= 0;\n const bIsPinned = bPin >= 0;\n // Pinned projects first, in their pinned order\n if (aIsPinned && bIsPinned) return aPin - bPin;\n if (aIsPinned) return -1;\n if (bIsPinned) return 1;\n // Unpinned: keep the original sort (by state)\n return 0;\n });\n }\n\n function escapeHtml(s) {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n }\n\n function timeAgo(epochMs) {\n if (!epochMs) return 'unknown';\n const s = Math.floor((Date.now() - epochMs) / 1000);\n if (s < 10) return 'just now';\n if (s < 60) return s + 's ago';\n if (s < 3600) return Math.floor(s / 60) + 'm ago';\n if (s < 86400) return Math.floor(s / 3600) + 'h ago';\n return Math.floor(s / 86400) + 'd ago';\n }\n\n function noteAgeColor(epochMs) {\n const m = (Date.now() - epochMs) / 60000;\n if (m < 10) return '#3fb950'; // green \u2014 fresh\n if (m < 60) return '#d29922'; // yellow \u2014 aging\n if (m < 360) return '#f0883e'; // orange \u2014 old\n return '#f85149'; // red \u2014 stale\n }\n\n function formatToolInput(input) {\n if (!input) return '';\n const hasDiff = input.includes('--- old') && input.includes('+++ new');\n if (hasDiff) {\n const lines = input.slice(0, 800).split('\\n');\n const html = lines.map(line => {\n const escaped = escapeHtml(line);\n if (line.startsWith('+++ new')) return '<span style=\"color:#3fb950;font-weight:600\">' + escaped + '</span>';\n if (line.startsWith('--- old')) return '<span style=\"color:#f85149;font-weight:600\">' + escaped + '</span>';\n if (line.startsWith('+')) return '<span style=\"color:#3fb950\">' + escaped + '</span>';\n if (line.startsWith('-')) return '<span style=\"color:#f85149\">' + escaped + '</span>';\n return escaped;\n }).join('\\n');\n return '<pre style=\"font-size:10px;color:#8b949e;word-break:break-all;white-space:pre-wrap;margin:4px 0;max-height:200px;overflow-y:auto\">' + html + '</pre>';\n }\n return '<pre style=\"font-size:10px;color:#8b949e;word-break:break-all;white-space:pre-wrap;margin:4px 0;max-height:200px;overflow-y:auto\">' + escapeHtml(input.slice(0, 3000)) + '</pre>';\n }\n\n function stateLabel(state) {\n const labels = {\n working: 'Working',\n waiting_permission: 'Permission',\n waiting_input: 'Waiting',\n idle: 'Idle',\n unknown: '?',\n };\n return labels[state] || state;\n }\n\n function renderSession(s) {\n const ageMs = Date.now() - s.lastActivity;\n const isStale = (s.state === 'idle' || s.state === 'unknown') && ageMs > 2 * 3600_000;\n const isAlert = s.state === 'waiting_permission' || s.state === 'waiting_input';\n const isActive = s.state === 'working';\n const cardClass = isStale ? 'stale' : isAlert ? 'alert' : isActive ? 'active' : '';\n\n let pending = '';\n if (s.pendingQuestion) {\n const q = s.pendingQuestion;\n const isPermission = q.type === 'permission_prompt';\n const contextInfo = q.context\n ? '<div style=\"font-size:11px;color:#c9d1d9;margin-bottom:6px;white-space:pre-wrap\">' + escapeHtml(q.context.slice(-300)) + '</div>'\n : '';\n const toolInfo = q.toolName\n ? contextInfo + '<span class=\"tool\">' + escapeHtml(q.toolName) + '</span>' +\n (q.toolInput ? '<br>' + formatToolInput(q.toolInput) : '')\n : '<div class=\"prompt-msg\" style=\"font-size:12px;color:#c9d1d9;white-space:pre-wrap;max-height:240px;overflow-y:auto\">' + escapeHtml(q.message) + '</div>';\n\n const actions = isPermission\n ? `<div class=\"action-row\">\n <button class=\"action-btn allow\" onclick=\"respondTo('${q.eventId}', 'allow', this)\">\u2713 Allow</button>\n <button class=\"action-btn deny\" onclick=\"respondTo('${q.eventId}', 'deny', this)\">\u2717 Deny</button>\n </div>`\n : `<div class=\"action-row\" style=\"align-items:center\">\n <input type=\"text\" class=\"reply-input\" id=\"reply-${q.eventId}\" placeholder=\"Type a reply...\" onkeydown=\"if(event.key==='Enter')respondTo('${q.eventId}',this.value,this)\">\n <button class=\"action-btn allow\" onclick=\"respondTo('${q.eventId}',document.getElementById('reply-${q.eventId}').value,this)\">Send</button>\n </div>`;\n\n pending = `\n <div class=\"pending-box\">\n <span class=\"ago\">${timeAgo(Date.now() - q.agoSeconds * 1000)}</span>\n ${toolInfo}\n ${actions}\n </div>\n `;\n }\n\n const hasGit = s.git.branch !== 'unknown';\n\n const gitParts = [];\n if (hasGit) {\n gitParts.push('<span class=\"git-branch\">' + escapeHtml(s.git.branch) + '</span>');\n gitParts.push(s.git.modifiedFiles > 0\n ? '<span class=\"git-modified\">' + s.git.modifiedFiles + ' mod</span>'\n : '<span class=\"git-clean\">clean</span>');\n if (s.git.unpushedCommits > 0) gitParts.push('<span class=\"git-unpushed\">' + s.git.unpushedCommits + ' unpush</span>');\n gitParts.push(s.committed\n ? '<span class=\"flag ok\">\u2713 commit</span>'\n : '<span class=\"flag pending\">\u25CB uncommit</span>');\n gitParts.push(s.pushed\n ? '<span class=\"flag ok\">\u2713 push</span>'\n : '<span class=\"flag pending\">\u25CB unpush</span>');\n }\n\n // Show reply input for idle/waiting sessions without a pending question\n const idleInput = (!s.pendingQuestion && (s.state === 'idle' || s.state === 'waiting_input' || s.state === 'unknown'))\n ? `<div class=\"action-row\" style=\"margin-top:6px\">\n <input type=\"text\" class=\"reply-input\" id=\"idle-${s.sessionId}\" placeholder=\"Send a message...\" onkeydown=\"if(event.key==='Enter')sendToSession('${s.sessionId}',this.value,this)\">\n <button class=\"action-btn allow\" onclick=\"sendToSession('${s.sessionId}',document.getElementById('idle-${s.sessionId}').value,this)\">Send</button>\n </div>`\n : '';\n\n const titleId = 'title-' + s.sessionId.slice(0, 8);\n const longTitle = s.title.length > 80;\n const expandBtn = longTitle ? `<button class=\"expand-btn\" onclick=\"document.getElementById('${titleId}').classList.toggle('expanded');this.textContent=this.textContent==='...'?'\u25B2':'...'\">...</button>` : '';\n\n return `\n <div class=\"card ${cardClass}\">\n <div class=\"card-header\">\n <span class=\"card-title\" id=\"${titleId}\">${escapeHtml(s.title)}</span>\n <span class=\"badge ${s.state}\">${stateLabel(s.state)}</span>\n <button class=\"dismiss-btn\" onclick=\"dismissSession('${s.sessionId}')\" title=\"Dismiss session\">\uD83D\uDDD1</button>\n </div>\n ${expandBtn}\n ${(() => {\n const sent = sentMessages.get(s.sessionId);\n if (sent && Date.now() - sent.at < 300_000) {\n const label = sent.icon === '\u25B6' ? 'Working on' : 'Replied';\n return '<div style=\"background:#0d2818;border:1px solid #238636;border-radius:6px;padding:6px 10px;margin:8px 0;font-size:12px;color:#c9d1d9;white-space:pre-wrap\">'\n + '<span style=\"color:#3fb950;font-weight:600\">' + label + ' :</span> '\n + escapeHtml(sent.text.length > 150 ? sent.text.slice(0, 150) + '...' : sent.text) + '</div>';\n }\n return '';\n })()}\n ${pending}\n ${idleInput}\n <div class=\"card-footer\">\n ${gitParts.join(' ')}\n <span class=\"spacer\"></span>\n <span>pane ${escapeHtml(s.tmuxPane)}</span>\n <span>${timeAgo(s.lastActivity)}</span>\n </div>\n </div>\n `;\n }\n\n function renderPipeline(label, pipeline) {\n if (!pipeline) return '';\n const s = pipeline.status;\n const dot = '<span class=\"ci-dot ' + s + '\"></span>';\n const text = label + ': ' + s;\n if (pipeline.url) {\n return '<a class=\"ci-badge ' + s + '\" href=\"' + escapeHtml(pipeline.url) + '\" target=\"_blank\">' + dot + ' ' + text + '</a>';\n }\n return '<span class=\"ci-badge ' + s + '\">' + dot + ' ' + text + '</span>';\n }\n\n function renderCI(ci) {\n if (!ci) return '';\n const main = renderPipeline('main', ci.main);\n const staging = renderPipeline('staging', ci.staging);\n return main + staging;\n }\n\n function renderNotes(project, notes) {\n if (!notes || notes.length === 0) {\n return `\n <div class=\"notes-panel\">\n <div class=\"notes-header\">Notes</div>\n <div class=\"note-add-row\">\n <input type=\"text\" class=\"reply-input\" id=\"note-add-${escapeHtml(project)}\" placeholder=\"Add a note...\" onkeydown=\"if(event.key==='Enter')addNote('${escapeHtml(project)}',this.value,this)\" onpaste=\"handleNotePaste(event,'${escapeHtml(project)}')\">\n <button class=\"note-btn send\" onclick=\"var i=document.getElementById('note-add-${escapeHtml(project)}');addNote('${escapeHtml(project)}',i.value,i)\">+</button>\n </div>\n </div>\n `;\n }\n\n const items = notes.map((n, idx) => {\n const thumb = n.image\n ? '<img src=\"/api/v1/notes/images/' + escapeHtml(n.image) + '\" class=\"note-thumb\" onclick=\"openNoteImage(this.src)\" title=\"Click to enlarge\">'\n : '';\n return `\n <div class=\"note-item\" draggable=\"true\" data-note-id=\"${n.id}\" data-project=\"${escapeHtml(project)}\"\n ondragstart=\"onNoteDragStart(event)\" ondragover=\"onNoteDragOver(event)\" ondrop=\"onNoteDrop(event)\" ondragend=\"onNoteDragEnd(event)\">\n <span class=\"note-grip\" title=\"Drag to reorder\">\u283F</span>\n ${thumb}\n <span class=\"note-text\" title=\"${escapeHtml(n.text)}\" onclick=\"editNote(this,'${n.id}')\">${escapeHtml(n.text)}</span>\n <span class=\"note-clock\" title=\"${timeAgo(n.createdAt)}\" style=\"color:${noteAgeColor(n.createdAt)}\">\u23F1</span>\n ${idx > 0 ? '<button class=\"note-btn move\" onclick=\"moveNote(\\'' + escapeHtml(project) + '\\',' + idx + ',-1)\" title=\"Move up\">\u25B2</button>' : '<span class=\"note-btn move\" style=\"visibility:hidden\">\u25B2</span>'}\n ${idx < notes.length - 1 ? '<button class=\"note-btn move\" onclick=\"moveNote(\\'' + escapeHtml(project) + '\\',' + idx + ',1)\" title=\"Move down\">\u25BC</button>' : '<span class=\"note-btn move\" style=\"visibility:hidden\">\u25BC</span>'}\n <button class=\"note-btn send\" onclick=\"sendNote('${n.id}',this)\" title=\"Send to session\">\u25B6</button>\n <button class=\"note-btn delete\" onclick=\"deleteNote('${n.id}')\" title=\"Delete\">\u2715</button>\n </div>\n `}).join('');\n\n return `\n <div class=\"notes-panel\">\n <div class=\"notes-header\">\n <span>Notes</span>\n <span class=\"count\">${notes.length}</span>\n </div>\n ${items}\n <div class=\"note-add-row\">\n <input type=\"text\" class=\"reply-input\" id=\"note-add-${escapeHtml(project)}\" placeholder=\"Add a note...\" onkeydown=\"if(event.key==='Enter')addNote('${escapeHtml(project)}',this.value,this)\" onpaste=\"handleNotePaste(event,'${escapeHtml(project)}')\">\n <button class=\"note-btn send\" onclick=\"var i=document.getElementById('note-add-${escapeHtml(project)}');addNote('${escapeHtml(project)}',i.value,i)\">+</button>\n </div>\n </div>\n `;\n }\n\n function renderProject(p) {\n const isPinned = getPinnedOrder().includes(p.name);\n const anyNeedsTesting = p.sessions.some(s => s.needsTesting);\n const testBadge = anyNeedsTesting ? '<span class=\"needs-testing\">needs testing</span>' : '';\n const ciBadges = renderCI(p.ci);\n const infoRow = (ciBadges || testBadge) ? '<div class=\"ci-row\">' + ciBadges + testBadge + '</div>' : '';\n\n return `\n <div class=\"project\">\n <div class=\"project-header\">\n <button class=\"pin-btn ${isPinned ? 'pinned' : ''}\" onclick=\"togglePin('${escapeHtml(p.name)}')\" title=\"${isPinned ? 'Unpin' : 'Pin'}\">${isPinned ? '\uD83D\uDCCC' : '\uD83D\uDCCC'}</button>\n <h2>${escapeHtml(p.name)}</h2>\n <span class=\"project-count\">${p.sessions.length} session${p.sessions.length > 1 ? 's' : ''}</span>\n <span class=\"project-path\">${escapeHtml(p.path)}</span>\n </div>\n ${infoRow}\n <div class=\"sessions\">\n ${p.sessions.map(renderSession).join('')}\n ${renderNotes(p.name, p.notes)}\n </div>\n </div>\n `;\n }\n\n function countPending(data) {\n let count = 0;\n for (const p of data.projects) {\n for (const s of p.sessions) {\n if (s.pendingQuestion && s.pendingQuestion.type === 'permission_prompt') count++;\n }\n }\n return count;\n }\n\n function render(data) {\n const container = document.getElementById('projects');\n if (!data.projects || data.projects.length === 0) {\n container.innerHTML = `\n <div class=\"empty\">\n <h2>No active sessions</h2>\n <p>Start Claude Code in tmux and sessions will appear here.</p>\n </div>\n `;\n document.getElementById('allowAllBtn').style.display = 'none';\n return;\n }\n\n const pendingCount = countPending(data);\n const allowAllBtn = document.getElementById('allowAllBtn');\n if (pendingCount > 1) {\n allowAllBtn.style.display = 'inline-block';\n allowAllBtn.textContent = 'Allow All (' + pendingCount + ')';\n } else {\n allowAllBtn.style.display = 'none';\n }\n\n // Skip DOM update if user is focused on an input field\n const focused = document.activeElement;\n if (focused && focused.tagName === 'INPUT' && focused.classList.contains('reply-input')) return;\n\n // Preserve input values and cursor position across re-renders\n const savedInputs = new Map();\n const focusedId = focused?.id;\n const cursorPos = focused?.selectionStart;\n document.querySelectorAll('.reply-input').forEach(inp => {\n if (inp.id && inp.value.trim()) savedInputs.set(inp.id, inp.value);\n });\n const expandedTitles = new Set();\n document.querySelectorAll('.card-title.expanded').forEach(el => expandedTitles.add(el.id));\n\n container.innerHTML = sortProjects(data.projects).map(renderProject).join('');\n\n // Autoscroll prompt messages to bottom \u2014 Claude puts the actual question\n // at the end of an idle_prompt, so reveal that part on render.\n document.querySelectorAll('.prompt-msg').forEach(el => { el.scrollTop = el.scrollHeight; });\n\n // Restore expanded titles\n expandedTitles.forEach(id => {\n const el = document.getElementById(id);\n if (el) {\n el.classList.add('expanded');\n const btn = el.parentElement?.querySelector('.expand-btn');\n if (btn) btn.textContent = '\u25B2';\n }\n });\n\n // Restore input values and focus\n savedInputs.forEach((val, id) => {\n const inp = document.getElementById(id);\n if (inp) inp.value = val;\n });\n if (focusedId) {\n const el = document.getElementById(focusedId);\n if (el) { el.focus(); if (cursorPos != null) el.selectionStart = el.selectionEnd = cursorPos; }\n }\n }\n\n function showSentBanner(card, icon, text, sessionId) {\n if (!card) return;\n // Track for persistent rendering across re-renders\n const sid = sessionId || card.querySelector('.card-title')?.id?.replace('title-', '');\n if (sid) {\n // Find full sessionId from data\n const fullId = data?.projects?.flatMap(p => p.sessions).find(s => s.sessionId.startsWith(sid))?.sessionId || sid;\n sentMessages.set(fullId, { icon, text, at: Date.now() });\n }\n }\n\n async function respondTo(eventId, response, btn) {\n if (btn) btn.disabled = true;\n const card = btn?.closest('.card');\n try {\n const res = await fetch('/api/v1/respond-to', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ eventId, response }),\n });\n if (res.ok) {\n const label = response === 'allow' ? '\u2713 Allowed' : response === 'deny' ? '\u2717 Denied' : response;\n showSentBanner(card, '\u21A9', label);\n fetchDashboard();\n } else {\n const err = await res.json();\n console.error('respond-to failed:', err);\n }\n } catch (e) {\n console.error('respond-to error:', e);\n }\n if (btn) btn.disabled = false;\n }\n\n async function dismissSession(sessionId) {\n try {\n await fetch('/api/v1/dismiss-session', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId }),\n });\n fetchDashboard();\n } catch (e) {\n console.error('dismiss error:', e);\n }\n }\n\n async function sendToSession(sessionId, text, btn) {\n if (!text || !text.trim()) return;\n if (btn) btn.disabled = true;\n const card = btn?.closest('.card');\n const input = card?.querySelector('input');\n if (input) { input.value = ''; input.blur(); }\n try {\n const res = await fetch('/api/v1/send-to', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, text: text.trim() }),\n });\n if (res.ok) {\n showSentBanner(card, '\u25B6', text.trim());\n fetchDashboard();\n } else {\n const err = await res.json();\n console.error('send-to failed:', err);\n }\n } catch (e) {\n console.error('send-to error:', e);\n }\n if (btn) btn.disabled = false;\n }\n\n let addingNote = false;\n async function addNote(project, text, input) {\n if (!text || !text.trim() || addingNote) return;\n addingNote = true;\n if (input) { input.value = ''; input.blur(); }\n try {\n await fetch('/api/v1/notes', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ project, text: text.trim(), source: 'dashboard' }),\n });\n fetchDashboard();\n } catch (e) {\n console.error('add-note error:', e);\n }\n addingNote = false;\n }\n\n async function handleNotePaste(event, project) {\n const items = event.clipboardData?.items;\n if (!items) return;\n for (const item of items) {\n if (item.type.startsWith('image/')) {\n event.preventDefault();\n const blob = item.getAsFile();\n if (!blob) return;\n const reader = new FileReader();\n reader.onload = async () => {\n const base64 = reader.result.split(',')[1];\n const text = event.target.value?.trim() || '';\n try {\n await fetch('/api/v1/notes/with-image', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ project, text: text || '(image)', imageBase64: base64, source: 'dashboard' }),\n });\n event.target.value = '';\n fetchDashboard();\n } catch (e) {\n console.error('paste-image error:', e);\n }\n };\n reader.readAsDataURL(blob);\n return;\n }\n }\n }\n\n function openNoteImage(src) {\n const lb = document.createElement('div');\n lb.className = 'note-lightbox';\n const img = document.createElement('img');\n img.src = src;\n lb.appendChild(img);\n lb.onclick = () => lb.remove();\n document.body.appendChild(lb);\n }\n\n function editNote(span, noteId) {\n const input = document.createElement('input');\n input.type = 'text';\n input.className = 'reply-input';\n input.value = span.textContent;\n input.style.cssText = 'flex:1;font-size:12px;';\n span.replaceWith(input);\n input.focus();\n input.select();\n async function save() {\n const text = input.value.trim();\n if (text && text !== span.textContent) {\n await fetch('/api/v1/notes/' + noteId, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ text }),\n });\n span.textContent = text;\n span.title = text;\n }\n input.replaceWith(span);\n }\n input.onblur = save;\n input.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); save(); } if (e.key === 'Escape') input.replaceWith(span); };\n }\n\n async function deleteNote(noteId) {\n try {\n await fetch('/api/v1/notes/' + noteId, { method: 'DELETE' });\n fetchDashboard();\n } catch (e) {\n console.error('delete-note error:', e);\n }\n }\n\n async function sendNote(noteId, btn) {\n if (btn) btn.disabled = true;\n const noteText = btn?.closest('.note-item')?.querySelector('.note-text')?.textContent || '';\n try {\n const res = await fetch('/api/v1/notes/' + noteId + '/send', { method: 'POST' });\n const result = await res.json();\n if (!res.ok) {\n showToast(btn, result.error || 'Failed to send', true);\n } else {\n if (result.sessionId) {\n const card = document.querySelector('#title-' + result.sessionId.slice(0, 8))?.closest('.card');\n showSentBanner(card, '\u25B6', noteText);\n }\n showToast(btn, 'Sent', false);\n }\n fetchDashboard();\n } catch (e) {\n console.error('send-note error:', e);\n }\n if (btn) btn.disabled = false;\n }\n\n function getProjectNoteIds(project) {\n return Array.from(document.querySelectorAll('.note-item[data-project=\"' + project + '\"]'))\n .map(el => el.dataset.noteId);\n }\n\n async function saveNoteOrder(project) {\n const orderedIds = getProjectNoteIds(project);\n try {\n await fetch('/api/v1/notes/reorder', {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ project, orderedIds }),\n });\n } catch (e) {\n console.error('reorder error:', e);\n }\n }\n\n function moveNote(project, idx, direction) {\n const items = document.querySelectorAll('.note-item[data-project=\"' + project + '\"]');\n const target = idx + direction;\n if (target < 0 || target >= items.length) return;\n const parent = items[0].parentNode;\n if (direction === -1) parent.insertBefore(items[idx], items[target]);\n else parent.insertBefore(items[target], items[idx]);\n saveNoteOrder(project);\n }\n\n let draggedNote = null;\n function onNoteDragStart(e) {\n draggedNote = e.currentTarget;\n draggedNote.classList.add('dragging');\n e.dataTransfer.effectAllowed = 'move';\n }\n function onNoteDragOver(e) {\n e.preventDefault();\n const item = e.currentTarget;\n if (item !== draggedNote) item.classList.add('drag-over');\n }\n function onNoteDrop(e) {\n e.preventDefault();\n const target = e.currentTarget;\n target.classList.remove('drag-over');\n if (!draggedNote || target === draggedNote) return;\n const parent = target.parentNode;\n const items = Array.from(parent.querySelectorAll('.note-item'));\n const fromIdx = items.indexOf(draggedNote);\n const toIdx = items.indexOf(target);\n if (fromIdx < toIdx) parent.insertBefore(draggedNote, target.nextSibling);\n else parent.insertBefore(draggedNote, target);\n saveNoteOrder(draggedNote.dataset.project);\n }\n function onNoteDragEnd(e) {\n document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));\n if (draggedNote) draggedNote.classList.remove('dragging');\n draggedNote = null;\n }\n\n function showToast(anchor, text, isError) {\n const toast = document.createElement('span');\n toast.textContent = (isError ? '\u2717 ' : '\u2713 ') + text;\n toast.style.cssText = 'position:absolute;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600;z-index:10;pointer-events:none;'\n + (isError ? 'background:#490202;color:#f85149;' : 'background:#0d2818;color:#3fb950;');\n if (anchor) {\n const rect = anchor.getBoundingClientRect();\n toast.style.left = rect.left + 'px';\n toast.style.top = (rect.top - 24) + 'px';\n } else {\n toast.style.right = '24px';\n toast.style.top = '70px';\n }\n document.body.appendChild(toast);\n setTimeout(() => toast.remove(), 2000);\n }\n\n async function allowAll() {\n if (!data) return;\n const pending = [];\n for (const p of data.projects) {\n for (const s of p.sessions) {\n if (s.pendingQuestion && s.pendingQuestion.type === 'permission_prompt') {\n pending.push(s.pendingQuestion);\n }\n }\n }\n for (const q of pending) {\n await respondTo(q.eventId, 'allow', null);\n }\n fetchDashboard();\n }\n\n async function fetchDashboard() {\n try {\n const res = await fetch('/api/v1/dashboard');\n data = await res.json();\n render(data);\n document.getElementById('statusDot').className = 'status-dot connected';\n document.getElementById('lastUpdate').textContent = 'updated ' + timeAgo(data.updatedAt);\n } catch {\n document.getElementById('statusDot').className = 'status-dot disconnected';\n document.getElementById('lastUpdate').textContent = 'disconnected';\n }\n }\n\n fetchDashboard();\n\n // SSE for instant push \u2014 polling as fallback\n let fallbackTimer = setInterval(fetchDashboard, 10000);\n function connectSSE() {\n const es = new EventSource('/api/v1/sse');\n es.addEventListener('refresh', () => fetchDashboard());\n es.onopen = () => {\n clearInterval(fallbackTimer);\n fallbackTimer = setInterval(fetchDashboard, 10000);\n };\n es.onerror = () => {\n es.close();\n clearInterval(fallbackTimer);\n fallbackTimer = setInterval(fetchDashboard, 2000);\n setTimeout(connectSSE, 3000);\n };\n }\n connectSSE();\n </script>\n</body>\n</html>";
1
+ export declare const DASHBOARD_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>claude-pager dashboard</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link href=\"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap\" rel=\"stylesheet\">\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n background: #0d1117;\n color: #c9d1d9;\n font-family: 'JetBrains Mono', monospace;\n min-height: 100vh;\n padding: 24px;\n }\n\n header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 32px;\n padding-bottom: 16px;\n border-bottom: 1px solid #21262d;\n }\n\n .logo {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n .logo h1 {\n font-size: 22px;\n font-weight: 700;\n color: #f0f6fc;\n }\n\n .cursor {\n display: inline-block;\n width: 10px;\n height: 20px;\n background: #58a6ff;\n animation: blink 1s step-end infinite;\n vertical-align: middle;\n margin-left: 4px;\n }\n\n @keyframes blink {\n 50% { opacity: 0; }\n }\n\n .status-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n display: inline-block;\n }\n\n .status-dot.connected { background: #3fb950; box-shadow: 0 0 6px #3fb950; }\n .status-dot.disconnected { background: #f85149; box-shadow: 0 0 6px #f85149; }\n\n .meta {\n font-size: 12px;\n color: #484f58;\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .project {\n margin-bottom: 28px;\n }\n\n .project-header {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 12px;\n }\n\n .project-header h2 {\n font-size: 16px;\n font-weight: 600;\n color: #58a6ff;\n }\n\n .project-count {\n background: #21262d;\n color: #8b949e;\n font-size: 11px;\n padding: 2px 8px;\n border-radius: 10px;\n }\n\n .project-path {\n font-size: 11px;\n color: #484f58;\n margin-left: auto;\n }\n\n .pin-btn {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 14px;\n opacity: 0.3;\n transition: opacity 0.2s;\n padding: 2px 4px;\n }\n\n .pin-btn:hover { opacity: 0.7; }\n .pin-btn.pinned { opacity: 1; }\n\n .dismiss-btn {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 12px;\n opacity: 0.25;\n transition: opacity 0.2s;\n padding: 2px 4px;\n }\n\n .dismiss-btn:hover { opacity: 0.8; color: #f85149; }\n\n .ci-row {\n display: flex;\n gap: 12px;\n margin-bottom: 12px;\n font-size: 11px;\n }\n\n .ci-badge {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 3px 10px;\n border-radius: 12px;\n font-weight: 600;\n text-decoration: none;\n transition: opacity 0.2s;\n }\n\n .ci-badge:hover { opacity: 0.8; }\n\n .ci-badge.success { background: #0d2818; color: #3fb950; }\n .ci-badge.failed { background: #490202; color: #f85149; }\n .ci-badge.running { background: #0d419d; color: #58a6ff; animation: pulse 2s ease-in-out infinite; }\n .ci-badge.pending { background: #3d2e00; color: #d29922; }\n .ci-badge.canceled { background: #21262d; color: #8b949e; }\n .ci-badge.unknown { background: #21262d; color: #484f58; }\n\n .ci-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n display: inline-block;\n }\n\n .ci-dot.success { background: #3fb950; }\n .ci-dot.failed { background: #f85149; }\n .ci-dot.running { background: #58a6ff; }\n .ci-dot.pending { background: #d29922; }\n .ci-dot.canceled { background: #8b949e; }\n .ci-dot.unknown { background: #484f58; }\n\n .sessions {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));\n gap: 12px;\n }\n\n .card {\n background: #161b22;\n border: 1px solid #21262d;\n border-radius: 8px;\n padding: 16px;\n transition: border-color 0.2s, box-shadow 0.2s, opacity 0.3s;\n }\n\n .card:hover {\n border-color: #388bfd44;\n box-shadow: 0 0 12px #388bfd22;\n }\n\n .card.stale {\n opacity: 0.45;\n border-style: dashed;\n }\n\n .card.stale:hover {\n opacity: 0.8;\n }\n\n .card.active {\n border-color: #388bfd44;\n border-left: 3px solid #58a6ff;\n }\n\n .card.alert {\n border-color: #f0883e44;\n border-left: 3px solid #f0883e;\n }\n\n .card-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n margin-bottom: 10px;\n }\n\n .card-title {\n font-size: 14px;\n font-weight: 600;\n color: #f0f6fc;\n line-height: 1.3;\n flex: 1;\n margin-right: 8px;\n overflow: hidden;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n }\n\n .card-title.expanded {\n -webkit-line-clamp: unset;\n overflow: visible;\n white-space: pre-wrap;\n }\n\n .expand-btn {\n background: none;\n border: none;\n color: #58a6ff;\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n cursor: pointer;\n padding: 2px 0;\n opacity: 0.8;\n }\n\n .expand-btn:hover { opacity: 1; }\n\n .badge {\n font-size: 10px;\n font-weight: 600;\n padding: 3px 8px;\n border-radius: 12px;\n white-space: nowrap;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .badge.working {\n background: #0d419d;\n color: #58a6ff;\n animation: pulse 2s ease-in-out infinite;\n }\n\n .badge.waiting_permission {\n background: #5a1e02;\n color: #f0883e;\n }\n\n .badge.waiting_input {\n background: #3d2e00;\n color: #d29922;\n }\n\n .badge.idle {\n background: #21262d;\n color: #8b949e;\n }\n\n .badge.unknown {\n background: #21262d;\n color: #484f58;\n }\n\n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.6; }\n }\n\n .pending-box {\n background: #1c1208;\n border: 1px solid #3d2e00;\n border-radius: 6px;\n padding: 8px 10px;\n margin-bottom: 10px;\n font-size: 12px;\n color: #d29922;\n }\n\n .pending-box .tool {\n color: #f0883e;\n font-weight: 600;\n }\n\n .pending-box .ago {\n color: #8b949e;\n float: right;\n }\n\n .action-row {\n display: flex;\n gap: 8px;\n margin-top: 8px;\n }\n\n .action-btn {\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n font-weight: 600;\n padding: 4px 14px;\n border-radius: 6px;\n border: none;\n cursor: pointer;\n transition: opacity 0.2s, transform 0.1s;\n }\n\n .action-btn:hover { opacity: 0.85; }\n .action-btn:active { transform: scale(0.96); }\n\n .action-btn.allow {\n background: #238636;\n color: #ffffff;\n }\n\n .action-btn.deny {\n background: #da3633;\n color: #ffffff;\n }\n\n .action-btn.allow-all {\n background: #1f6feb;\n color: #ffffff;\n margin-left: auto;\n }\n\n .action-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .reply-input {\n flex: 1;\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n padding: 4px 10px;\n border-radius: 6px;\n border: 1px solid #30363d;\n background: #0d1117;\n color: #c9d1d9;\n outline: none;\n }\n\n .reply-input:focus {\n border-color: #58a6ff;\n }\n\n .git-row {\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 11px;\n margin-bottom: 6px;\n }\n\n .git-branch {\n color: #8b949e;\n }\n\n .git-branch::before {\n content: '\u2387 ';\n }\n\n .git-modified {\n color: #f85149;\n }\n\n .git-unpushed {\n color: #d29922;\n }\n\n .git-clean {\n color: #3fb950;\n }\n\n .needs-testing {\n display: inline-block;\n font-size: 10px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n background: #490202;\n color: #f85149;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .flag {\n display: inline-block;\n font-size: 10px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n letter-spacing: 0.3px;\n }\n\n .flag.ok {\n background: #0d2818;\n color: #3fb950;\n }\n\n .flag.pending {\n background: #3d2e00;\n color: #d29922;\n }\n\n .card-footer {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 8px;\n font-size: 10px;\n color: #484f58;\n }\n\n .card-footer .spacer {\n margin-left: auto;\n }\n\n .notes-panel {\n background: #1a1e2e;\n border: 1px solid #2d333b;\n border-radius: 8px;\n padding: 12px;\n min-width: 0;\n }\n\n .notes-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 8px;\n font-size: 12px;\n font-weight: 600;\n color: #8b949e;\n }\n\n .notes-header .count {\n background: #2d333b;\n color: #c9d1d9;\n font-size: 10px;\n padding: 1px 7px;\n border-radius: 8px;\n }\n\n .note-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n border-radius: 4px;\n font-size: 12px;\n color: #c9d1d9;\n transition: background 0.15s;\n }\n\n .note-item:hover {\n background: #21262d;\n }\n\n .note-item .note-text {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: text;\n }\n\n .note-clock {\n font-size: 11px;\n cursor: default;\n flex-shrink: 0;\n }\n\n .note-thumb {\n width: 32px;\n height: 32px;\n object-fit: cover;\n border-radius: 4px;\n cursor: pointer;\n flex-shrink: 0;\n border: 1px solid #30363d;\n transition: transform 0.15s;\n }\n\n .note-thumb:hover {\n transform: scale(1.1);\n border-color: #58a6ff;\n }\n\n .note-lightbox {\n position: fixed;\n top: 0; left: 0; right: 0; bottom: 0;\n background: rgba(0,0,0,0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n cursor: pointer;\n }\n\n .note-lightbox img {\n max-width: 90vw;\n max-height: 90vh;\n border-radius: 8px;\n box-shadow: 0 0 40px rgba(0,0,0,0.5);\n }\n\n .note-grip {\n cursor: grab;\n color: #484f58;\n font-size: 10px;\n user-select: none;\n }\n .note-item.dragging {\n opacity: 0.4;\n }\n .note-item.drag-over {\n border-top: 2px solid #58a6ff;\n margin-top: -2px;\n }\n .note-btn.move {\n background: none;\n color: #484f58;\n padding: 0 2px;\n font-size: 8px;\n min-width: 16px;\n }\n .note-btn.move:hover {\n color: #58a6ff;\n }\n .note-btn {\n font-family: 'JetBrains Mono', monospace;\n font-size: 10px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 4px;\n border: none;\n cursor: pointer;\n transition: opacity 0.2s;\n }\n\n .note-btn:hover { opacity: 0.85; }\n\n .note-btn.send {\n background: #238636;\n color: #fff;\n }\n\n .note-btn.delete {\n background: #21262d;\n color: #8b949e;\n }\n\n .note-btn.delete:hover {\n background: #da3633;\n color: #fff;\n }\n\n .note-add-row {\n display: flex;\n gap: 6px;\n margin-top: 8px;\n }\n\n .empty {\n text-align: center;\n padding: 60px 20px;\n color: #484f58;\n }\n\n .empty h2 {\n font-size: 18px;\n color: #8b949e;\n margin-bottom: 8px;\n }\n\n .empty p {\n font-size: 13px;\n }\n\n @media (max-width: 768px) {\n body { padding: 12px; }\n\n header { flex-direction: column; align-items: flex-start; gap: 8px; }\n\n .logo h1 { font-size: 18px; }\n\n .sessions {\n grid-template-columns: 1fr;\n gap: 10px;\n }\n\n .project-header {\n flex-wrap: wrap;\n }\n\n .project-path { display: none; }\n\n .card { padding: 12px; }\n\n .card-title { font-size: 13px; }\n\n .action-btn {\n padding: 8px 18px;\n font-size: 13px;\n }\n\n .reply-input {\n font-size: 13px;\n padding: 8px 10px;\n }\n\n .ci-row { flex-wrap: wrap; gap: 6px; }\n\n .pending-box { font-size: 11px; }\n\n .pending-box code { font-size: 9px; }\n }\n\n @media (max-width: 480px) {\n body { padding: 8px; }\n\n .logo h1 { font-size: 16px; }\n\n .badge { font-size: 9px; padding: 2px 6px; }\n\n .git-row { flex-wrap: wrap; gap: 6px; }\n\n .action-btn {\n padding: 10px 20px;\n font-size: 14px;\n }\n\n .action-btn.allow-all {\n width: 100%;\n text-align: center;\n }\n }\n\n .scanline {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n background: repeating-linear-gradient(\n 0deg,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.03) 2px,\n rgba(0, 0, 0, 0.03) 4px\n );\n z-index: 999;\n }\n </style>\n</head>\n<body>\n <div class=\"scanline\"></div>\n <header>\n <div class=\"logo\">\n <h1>claude-pager<span class=\"cursor\"></span></h1>\n </div>\n <div class=\"meta\">\n <button class=\"action-btn allow-all\" id=\"allowAllBtn\" style=\"display:none\" onclick=\"allowAll()\">Allow All</button>\n <span class=\"status-dot connected\" id=\"statusDot\"></span>\n <span id=\"lastUpdate\">connecting...</span>\n </div>\n </header>\n <main id=\"projects\"></main>\n\n <script>\n let data = null;\n const sentMessages = new Map(); // sessionId \u2192 { icon, text, at }\n\n function getPinnedOrder() {\n try { return JSON.parse(localStorage.getItem('dashboard-pin-order') || '[]'); }\n catch { return []; }\n }\n\n function savePinnedOrder(order) {\n localStorage.setItem('dashboard-pin-order', JSON.stringify(order));\n }\n\n function togglePin(name) {\n const order = getPinnedOrder();\n const idx = order.indexOf(name);\n if (idx >= 0) {\n order.splice(idx, 1);\n } else {\n order.push(name);\n }\n savePinnedOrder(order);\n if (data) render(data);\n }\n\n function sortProjects(projects) {\n const pinned = getPinnedOrder();\n return [...projects].sort((a, b) => {\n const aPin = pinned.indexOf(a.name);\n const bPin = pinned.indexOf(b.name);\n const aIsPinned = aPin >= 0;\n const bIsPinned = bPin >= 0;\n // Pinned projects first, in their pinned order\n if (aIsPinned && bIsPinned) return aPin - bPin;\n if (aIsPinned) return -1;\n if (bIsPinned) return 1;\n // Unpinned: keep the original sort (by state)\n return 0;\n });\n }\n\n function escapeHtml(s) {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n }\n\n function timeAgo(epochMs) {\n if (!epochMs) return 'unknown';\n const s = Math.floor((Date.now() - epochMs) / 1000);\n if (s < 10) return 'just now';\n if (s < 60) return s + 's ago';\n if (s < 3600) return Math.floor(s / 60) + 'm ago';\n if (s < 86400) return Math.floor(s / 3600) + 'h ago';\n return Math.floor(s / 86400) + 'd ago';\n }\n\n function noteAgeColor(epochMs) {\n const m = (Date.now() - epochMs) / 60000;\n if (m < 10) return '#3fb950'; // green \u2014 fresh\n if (m < 60) return '#d29922'; // yellow \u2014 aging\n if (m < 360) return '#f0883e'; // orange \u2014 old\n return '#f85149'; // red \u2014 stale\n }\n\n function formatToolInput(input) {\n if (!input) return '';\n const hasDiff = input.includes('--- old') && input.includes('+++ new');\n if (hasDiff) {\n const lines = input.slice(0, 800).split('\\n');\n const html = lines.map(line => {\n const escaped = escapeHtml(line);\n if (line.startsWith('+++ new')) return '<span style=\"color:#3fb950;font-weight:600\">' + escaped + '</span>';\n if (line.startsWith('--- old')) return '<span style=\"color:#f85149;font-weight:600\">' + escaped + '</span>';\n if (line.startsWith('+')) return '<span style=\"color:#3fb950\">' + escaped + '</span>';\n if (line.startsWith('-')) return '<span style=\"color:#f85149\">' + escaped + '</span>';\n return escaped;\n }).join('\\n');\n return '<pre style=\"font-size:10px;color:#8b949e;word-break:break-all;white-space:pre-wrap;margin:4px 0;max-height:200px;overflow-y:auto\">' + html + '</pre>';\n }\n return '<pre style=\"font-size:10px;color:#8b949e;word-break:break-all;white-space:pre-wrap;margin:4px 0;max-height:200px;overflow-y:auto\">' + escapeHtml(input.slice(0, 3000)) + '</pre>';\n }\n\n function stateLabel(state) {\n const labels = {\n working: 'Working',\n waiting_permission: 'Permission',\n waiting_input: 'Waiting',\n idle: 'Idle',\n unknown: '?',\n };\n return labels[state] || state;\n }\n\n function renderSession(s) {\n const ageMs = Date.now() - s.lastActivity;\n const isStale = (s.state === 'idle' || s.state === 'unknown') && ageMs > 2 * 3600_000;\n const isAlert = s.state === 'waiting_permission' || s.state === 'waiting_input';\n const isActive = s.state === 'working';\n const cardClass = isStale ? 'stale' : isAlert ? 'alert' : isActive ? 'active' : '';\n\n let pending = '';\n if (s.pendingQuestion) {\n const q = s.pendingQuestion;\n const isPermission = q.type === 'permission_prompt';\n const contextInfo = q.context\n ? '<div style=\"font-size:11px;color:#c9d1d9;margin-bottom:6px;white-space:pre-wrap\">' + escapeHtml(q.context.slice(-300)) + '</div>'\n : '';\n const toolInfo = q.toolName\n ? contextInfo + '<span class=\"tool\">' + escapeHtml(q.toolName) + '</span>' +\n (q.toolInput ? '<br>' + formatToolInput(q.toolInput) : '')\n : '<div class=\"prompt-msg\" style=\"font-size:12px;color:#c9d1d9;white-space:pre-wrap;max-height:240px;overflow-y:auto\">' + escapeHtml(q.message) + '</div>';\n\n const actions = isPermission\n ? `<div class=\"action-row\">\n <button class=\"action-btn allow\" onclick=\"respondTo('${q.eventId}', 'allow', this)\">\u2713 Allow</button>\n <button class=\"action-btn deny\" onclick=\"respondTo('${q.eventId}', 'deny', this)\">\u2717 Deny</button>\n </div>`\n : `<div class=\"action-row\" style=\"align-items:center\">\n <input type=\"text\" class=\"reply-input\" id=\"reply-${q.eventId}\" placeholder=\"Type a reply...\" onkeydown=\"if(event.key==='Enter')respondTo('${q.eventId}',this.value,this)\">\n <button class=\"action-btn allow\" onclick=\"respondTo('${q.eventId}',document.getElementById('reply-${q.eventId}').value,this)\">Send</button>\n </div>`;\n\n pending = `\n <div class=\"pending-box\">\n <span class=\"ago\">${timeAgo(Date.now() - q.agoSeconds * 1000)}</span>\n ${toolInfo}\n ${actions}\n </div>\n `;\n }\n\n const hasGit = s.git.branch !== 'unknown';\n\n const gitParts = [];\n if (hasGit) {\n gitParts.push('<span class=\"git-branch\">' + escapeHtml(s.git.branch) + '</span>');\n gitParts.push(s.git.modifiedFiles > 0\n ? '<span class=\"git-modified\">' + s.git.modifiedFiles + ' mod</span>'\n : '<span class=\"git-clean\">clean</span>');\n if (s.git.unpushedCommits > 0) gitParts.push('<span class=\"git-unpushed\">' + s.git.unpushedCommits + ' unpush</span>');\n gitParts.push(s.committed\n ? '<span class=\"flag ok\">\u2713 commit</span>'\n : '<span class=\"flag pending\">\u25CB uncommit</span>');\n gitParts.push(s.pushed\n ? '<span class=\"flag ok\">\u2713 push</span>'\n : '<span class=\"flag pending\">\u25CB unpush</span>');\n }\n\n // Show reply input for idle/waiting sessions without a pending question\n const idleInput = (!s.pendingQuestion && (s.state === 'idle' || s.state === 'waiting_input' || s.state === 'unknown'))\n ? `<div class=\"action-row\" style=\"margin-top:6px\">\n <input type=\"text\" class=\"reply-input\" id=\"idle-${s.sessionId}\" placeholder=\"Send a message...\" onkeydown=\"if(event.key==='Enter')sendToSession('${s.sessionId}',this.value,this)\">\n <button class=\"action-btn allow\" onclick=\"sendToSession('${s.sessionId}',document.getElementById('idle-${s.sessionId}').value,this)\">Send</button>\n </div>`\n : '';\n\n const titleId = 'title-' + s.sessionId.slice(0, 8);\n const longTitle = s.title.length > 80;\n const expandBtn = longTitle ? `<button class=\"expand-btn\" onclick=\"document.getElementById('${titleId}').classList.toggle('expanded');this.textContent=this.textContent==='...'?'\u25B2':'...'\">...</button>` : '';\n\n return `\n <div class=\"card ${cardClass}\">\n <div class=\"card-header\">\n <span class=\"card-title\" id=\"${titleId}\">${escapeHtml(s.title)}</span>\n <span class=\"badge ${s.state}\">${stateLabel(s.state)}</span>\n <button class=\"dismiss-btn\" onclick=\"dismissSession('${s.sessionId}')\" title=\"Dismiss session\">\uD83D\uDDD1</button>\n </div>\n ${expandBtn}\n ${(() => {\n const sent = sentMessages.get(s.sessionId);\n if (sent && Date.now() - sent.at < 300_000) {\n const label = sent.icon === '\u25B6' ? 'Working on' : 'Replied';\n return '<div style=\"background:#0d2818;border:1px solid #238636;border-radius:6px;padding:6px 10px;margin:8px 0;font-size:12px;color:#c9d1d9;white-space:pre-wrap\">'\n + '<span style=\"color:#3fb950;font-weight:600\">' + label + ' :</span> '\n + escapeHtml(sent.text.length > 150 ? sent.text.slice(0, 150) + '...' : sent.text) + '</div>';\n }\n return '';\n })()}\n ${pending}\n ${s.lastAssistantText\n ? '<div class=\"prompt-msg\" style=\"font-size:12px;color:#c9d1d9;white-space:pre-wrap;max-height:240px;overflow-y:auto;background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:8px 10px;margin:8px 0\">' + escapeHtml(s.lastAssistantText) + '</div>'\n : ''}\n ${idleInput}\n <div class=\"card-footer\">\n ${gitParts.join(' ')}\n <span class=\"spacer\"></span>\n <span>pane ${escapeHtml(s.tmuxPane)}</span>\n <span>${timeAgo(s.lastActivity)}</span>\n </div>\n </div>\n `;\n }\n\n function renderPipeline(label, pipeline) {\n if (!pipeline) return '';\n const s = pipeline.status;\n const dot = '<span class=\"ci-dot ' + s + '\"></span>';\n const text = label + ': ' + s;\n if (pipeline.url) {\n return '<a class=\"ci-badge ' + s + '\" href=\"' + escapeHtml(pipeline.url) + '\" target=\"_blank\">' + dot + ' ' + text + '</a>';\n }\n return '<span class=\"ci-badge ' + s + '\">' + dot + ' ' + text + '</span>';\n }\n\n function renderCI(ci) {\n if (!ci) return '';\n const main = renderPipeline('main', ci.main);\n const staging = renderPipeline('staging', ci.staging);\n return main + staging;\n }\n\n function renderNotes(project, notes) {\n if (!notes || notes.length === 0) {\n return `\n <div class=\"notes-panel\">\n <div class=\"notes-header\">Notes</div>\n <div class=\"note-add-row\">\n <input type=\"text\" class=\"reply-input\" id=\"note-add-${escapeHtml(project)}\" placeholder=\"Add a note...\" onkeydown=\"if(event.key==='Enter')addNote('${escapeHtml(project)}',this.value,this)\" onpaste=\"handleNotePaste(event,'${escapeHtml(project)}')\">\n <button class=\"note-btn send\" onclick=\"var i=document.getElementById('note-add-${escapeHtml(project)}');addNote('${escapeHtml(project)}',i.value,i)\">+</button>\n </div>\n </div>\n `;\n }\n\n const items = notes.map((n, idx) => {\n const thumb = n.image\n ? '<img src=\"/api/v1/notes/images/' + escapeHtml(n.image) + '\" class=\"note-thumb\" onclick=\"openNoteImage(this.src)\" title=\"Click to enlarge\">'\n : '';\n return `\n <div class=\"note-item\" draggable=\"true\" data-note-id=\"${n.id}\" data-project=\"${escapeHtml(project)}\"\n ondragstart=\"onNoteDragStart(event)\" ondragover=\"onNoteDragOver(event)\" ondrop=\"onNoteDrop(event)\" ondragend=\"onNoteDragEnd(event)\">\n <span class=\"note-grip\" title=\"Drag to reorder\">\u283F</span>\n ${thumb}\n <span class=\"note-text\" title=\"${escapeHtml(n.text)}\" onclick=\"editNote(this,'${n.id}')\">${escapeHtml(n.text)}</span>\n <span class=\"note-clock\" title=\"${timeAgo(n.createdAt)}\" style=\"color:${noteAgeColor(n.createdAt)}\">\u23F1</span>\n ${idx > 0 ? '<button class=\"note-btn move\" onclick=\"moveNote(\\'' + escapeHtml(project) + '\\',' + idx + ',-1)\" title=\"Move up\">\u25B2</button>' : '<span class=\"note-btn move\" style=\"visibility:hidden\">\u25B2</span>'}\n ${idx < notes.length - 1 ? '<button class=\"note-btn move\" onclick=\"moveNote(\\'' + escapeHtml(project) + '\\',' + idx + ',1)\" title=\"Move down\">\u25BC</button>' : '<span class=\"note-btn move\" style=\"visibility:hidden\">\u25BC</span>'}\n <button class=\"note-btn send\" onclick=\"sendNote('${n.id}',this)\" title=\"Send to session\">\u25B6</button>\n <button class=\"note-btn delete\" onclick=\"deleteNote('${n.id}')\" title=\"Delete\">\u2715</button>\n </div>\n `}).join('');\n\n return `\n <div class=\"notes-panel\">\n <div class=\"notes-header\">\n <span>Notes</span>\n <span class=\"count\">${notes.length}</span>\n </div>\n ${items}\n <div class=\"note-add-row\">\n <input type=\"text\" class=\"reply-input\" id=\"note-add-${escapeHtml(project)}\" placeholder=\"Add a note...\" onkeydown=\"if(event.key==='Enter')addNote('${escapeHtml(project)}',this.value,this)\" onpaste=\"handleNotePaste(event,'${escapeHtml(project)}')\">\n <button class=\"note-btn send\" onclick=\"var i=document.getElementById('note-add-${escapeHtml(project)}');addNote('${escapeHtml(project)}',i.value,i)\">+</button>\n </div>\n </div>\n `;\n }\n\n function renderProject(p) {\n const isPinned = getPinnedOrder().includes(p.name);\n const anyNeedsTesting = p.sessions.some(s => s.needsTesting);\n const testBadge = anyNeedsTesting ? '<span class=\"needs-testing\">needs testing</span>' : '';\n const ciBadges = renderCI(p.ci);\n const infoRow = (ciBadges || testBadge) ? '<div class=\"ci-row\">' + ciBadges + testBadge + '</div>' : '';\n\n return `\n <div class=\"project\">\n <div class=\"project-header\">\n <button class=\"pin-btn ${isPinned ? 'pinned' : ''}\" onclick=\"togglePin('${escapeHtml(p.name)}')\" title=\"${isPinned ? 'Unpin' : 'Pin'}\">${isPinned ? '\uD83D\uDCCC' : '\uD83D\uDCCC'}</button>\n <h2>${escapeHtml(p.name)}</h2>\n <span class=\"project-count\">${p.sessions.length} session${p.sessions.length > 1 ? 's' : ''}</span>\n <span class=\"project-path\">${escapeHtml(p.path)}</span>\n </div>\n ${infoRow}\n <div class=\"sessions\">\n ${p.sessions.map(renderSession).join('')}\n ${renderNotes(p.name, p.notes)}\n </div>\n </div>\n `;\n }\n\n function countPending(data) {\n let count = 0;\n for (const p of data.projects) {\n for (const s of p.sessions) {\n if (s.pendingQuestion && s.pendingQuestion.type === 'permission_prompt') count++;\n }\n }\n return count;\n }\n\n function render(data) {\n const container = document.getElementById('projects');\n if (!data.projects || data.projects.length === 0) {\n container.innerHTML = `\n <div class=\"empty\">\n <h2>No active sessions</h2>\n <p>Start Claude Code in tmux and sessions will appear here.</p>\n </div>\n `;\n document.getElementById('allowAllBtn').style.display = 'none';\n return;\n }\n\n const pendingCount = countPending(data);\n const allowAllBtn = document.getElementById('allowAllBtn');\n if (pendingCount > 1) {\n allowAllBtn.style.display = 'inline-block';\n allowAllBtn.textContent = 'Allow All (' + pendingCount + ')';\n } else {\n allowAllBtn.style.display = 'none';\n }\n\n // Skip DOM update if user is focused on an input field\n const focused = document.activeElement;\n if (focused && focused.tagName === 'INPUT' && focused.classList.contains('reply-input')) return;\n\n // Preserve input values and cursor position across re-renders\n const savedInputs = new Map();\n const focusedId = focused?.id;\n const cursorPos = focused?.selectionStart;\n document.querySelectorAll('.reply-input').forEach(inp => {\n if (inp.id && inp.value.trim()) savedInputs.set(inp.id, inp.value);\n });\n const expandedTitles = new Set();\n document.querySelectorAll('.card-title.expanded').forEach(el => expandedTitles.add(el.id));\n\n container.innerHTML = sortProjects(data.projects).map(renderProject).join('');\n\n // Autoscroll prompt messages to bottom \u2014 Claude puts the actual question\n // at the end of an idle_prompt, so reveal that part on render.\n document.querySelectorAll('.prompt-msg').forEach(el => { el.scrollTop = el.scrollHeight; });\n\n // Restore expanded titles\n expandedTitles.forEach(id => {\n const el = document.getElementById(id);\n if (el) {\n el.classList.add('expanded');\n const btn = el.parentElement?.querySelector('.expand-btn');\n if (btn) btn.textContent = '\u25B2';\n }\n });\n\n // Restore input values and focus\n savedInputs.forEach((val, id) => {\n const inp = document.getElementById(id);\n if (inp) inp.value = val;\n });\n if (focusedId) {\n const el = document.getElementById(focusedId);\n if (el) { el.focus(); if (cursorPos != null) el.selectionStart = el.selectionEnd = cursorPos; }\n }\n }\n\n function showSentBanner(card, icon, text, sessionId) {\n if (!card) return;\n // Track for persistent rendering across re-renders\n const sid = sessionId || card.querySelector('.card-title')?.id?.replace('title-', '');\n if (sid) {\n // Find full sessionId from data\n const fullId = data?.projects?.flatMap(p => p.sessions).find(s => s.sessionId.startsWith(sid))?.sessionId || sid;\n sentMessages.set(fullId, { icon, text, at: Date.now() });\n }\n }\n\n async function respondTo(eventId, response, btn) {\n if (btn) btn.disabled = true;\n const card = btn?.closest('.card');\n try {\n const res = await fetch('/api/v1/respond-to', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ eventId, response }),\n });\n if (res.ok) {\n const label = response === 'allow' ? '\u2713 Allowed' : response === 'deny' ? '\u2717 Denied' : response;\n showSentBanner(card, '\u21A9', label);\n fetchDashboard();\n } else {\n const err = await res.json();\n console.error('respond-to failed:', err);\n }\n } catch (e) {\n console.error('respond-to error:', e);\n }\n if (btn) btn.disabled = false;\n }\n\n async function dismissSession(sessionId) {\n try {\n await fetch('/api/v1/dismiss-session', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId }),\n });\n fetchDashboard();\n } catch (e) {\n console.error('dismiss error:', e);\n }\n }\n\n async function sendToSession(sessionId, text, btn) {\n if (!text || !text.trim()) return;\n if (btn) btn.disabled = true;\n const card = btn?.closest('.card');\n const input = card?.querySelector('input');\n if (input) { input.value = ''; input.blur(); }\n try {\n const res = await fetch('/api/v1/send-to', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, text: text.trim() }),\n });\n if (res.ok) {\n showSentBanner(card, '\u25B6', text.trim());\n fetchDashboard();\n } else {\n const err = await res.json();\n console.error('send-to failed:', err);\n }\n } catch (e) {\n console.error('send-to error:', e);\n }\n if (btn) btn.disabled = false;\n }\n\n let addingNote = false;\n async function addNote(project, text, input) {\n if (!text || !text.trim() || addingNote) return;\n addingNote = true;\n if (input) { input.value = ''; input.blur(); }\n try {\n await fetch('/api/v1/notes', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ project, text: text.trim(), source: 'dashboard' }),\n });\n fetchDashboard();\n } catch (e) {\n console.error('add-note error:', e);\n }\n addingNote = false;\n }\n\n async function handleNotePaste(event, project) {\n const items = event.clipboardData?.items;\n if (!items) return;\n for (const item of items) {\n if (item.type.startsWith('image/')) {\n event.preventDefault();\n const blob = item.getAsFile();\n if (!blob) return;\n const reader = new FileReader();\n reader.onload = async () => {\n const base64 = reader.result.split(',')[1];\n const text = event.target.value?.trim() || '';\n try {\n await fetch('/api/v1/notes/with-image', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ project, text: text || '(image)', imageBase64: base64, source: 'dashboard' }),\n });\n event.target.value = '';\n fetchDashboard();\n } catch (e) {\n console.error('paste-image error:', e);\n }\n };\n reader.readAsDataURL(blob);\n return;\n }\n }\n }\n\n function openNoteImage(src) {\n const lb = document.createElement('div');\n lb.className = 'note-lightbox';\n const img = document.createElement('img');\n img.src = src;\n lb.appendChild(img);\n lb.onclick = () => lb.remove();\n document.body.appendChild(lb);\n }\n\n function editNote(span, noteId) {\n const input = document.createElement('input');\n input.type = 'text';\n input.className = 'reply-input';\n input.value = span.textContent;\n input.style.cssText = 'flex:1;font-size:12px;';\n span.replaceWith(input);\n input.focus();\n input.select();\n async function save() {\n const text = input.value.trim();\n if (text && text !== span.textContent) {\n await fetch('/api/v1/notes/' + noteId, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ text }),\n });\n span.textContent = text;\n span.title = text;\n }\n input.replaceWith(span);\n }\n input.onblur = save;\n input.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); save(); } if (e.key === 'Escape') input.replaceWith(span); };\n }\n\n async function deleteNote(noteId) {\n try {\n await fetch('/api/v1/notes/' + noteId, { method: 'DELETE' });\n fetchDashboard();\n } catch (e) {\n console.error('delete-note error:', e);\n }\n }\n\n async function sendNote(noteId, btn) {\n if (btn) btn.disabled = true;\n const noteText = btn?.closest('.note-item')?.querySelector('.note-text')?.textContent || '';\n try {\n const res = await fetch('/api/v1/notes/' + noteId + '/send', { method: 'POST' });\n const result = await res.json();\n if (!res.ok) {\n showToast(btn, result.error || 'Failed to send', true);\n } else {\n if (result.sessionId) {\n const card = document.querySelector('#title-' + result.sessionId.slice(0, 8))?.closest('.card');\n showSentBanner(card, '\u25B6', noteText);\n }\n showToast(btn, 'Sent', false);\n }\n fetchDashboard();\n } catch (e) {\n console.error('send-note error:', e);\n }\n if (btn) btn.disabled = false;\n }\n\n function getProjectNoteIds(project) {\n return Array.from(document.querySelectorAll('.note-item[data-project=\"' + project + '\"]'))\n .map(el => el.dataset.noteId);\n }\n\n async function saveNoteOrder(project) {\n const orderedIds = getProjectNoteIds(project);\n try {\n await fetch('/api/v1/notes/reorder', {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ project, orderedIds }),\n });\n } catch (e) {\n console.error('reorder error:', e);\n }\n }\n\n function moveNote(project, idx, direction) {\n const items = document.querySelectorAll('.note-item[data-project=\"' + project + '\"]');\n const target = idx + direction;\n if (target < 0 || target >= items.length) return;\n const parent = items[0].parentNode;\n if (direction === -1) parent.insertBefore(items[idx], items[target]);\n else parent.insertBefore(items[target], items[idx]);\n saveNoteOrder(project);\n }\n\n let draggedNote = null;\n function onNoteDragStart(e) {\n draggedNote = e.currentTarget;\n draggedNote.classList.add('dragging');\n e.dataTransfer.effectAllowed = 'move';\n }\n function onNoteDragOver(e) {\n e.preventDefault();\n const item = e.currentTarget;\n if (item !== draggedNote) item.classList.add('drag-over');\n }\n function onNoteDrop(e) {\n e.preventDefault();\n const target = e.currentTarget;\n target.classList.remove('drag-over');\n if (!draggedNote || target === draggedNote) return;\n const parent = target.parentNode;\n const items = Array.from(parent.querySelectorAll('.note-item'));\n const fromIdx = items.indexOf(draggedNote);\n const toIdx = items.indexOf(target);\n if (fromIdx < toIdx) parent.insertBefore(draggedNote, target.nextSibling);\n else parent.insertBefore(draggedNote, target);\n saveNoteOrder(draggedNote.dataset.project);\n }\n function onNoteDragEnd(e) {\n document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));\n if (draggedNote) draggedNote.classList.remove('dragging');\n draggedNote = null;\n }\n\n function showToast(anchor, text, isError) {\n const toast = document.createElement('span');\n toast.textContent = (isError ? '\u2717 ' : '\u2713 ') + text;\n toast.style.cssText = 'position:absolute;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600;z-index:10;pointer-events:none;'\n + (isError ? 'background:#490202;color:#f85149;' : 'background:#0d2818;color:#3fb950;');\n if (anchor) {\n const rect = anchor.getBoundingClientRect();\n toast.style.left = rect.left + 'px';\n toast.style.top = (rect.top - 24) + 'px';\n } else {\n toast.style.right = '24px';\n toast.style.top = '70px';\n }\n document.body.appendChild(toast);\n setTimeout(() => toast.remove(), 2000);\n }\n\n async function allowAll() {\n if (!data) return;\n const pending = [];\n for (const p of data.projects) {\n for (const s of p.sessions) {\n if (s.pendingQuestion && s.pendingQuestion.type === 'permission_prompt') {\n pending.push(s.pendingQuestion);\n }\n }\n }\n for (const q of pending) {\n await respondTo(q.eventId, 'allow', null);\n }\n fetchDashboard();\n }\n\n async function fetchDashboard() {\n try {\n const res = await fetch('/api/v1/dashboard');\n data = await res.json();\n render(data);\n document.getElementById('statusDot').className = 'status-dot connected';\n document.getElementById('lastUpdate').textContent = 'updated ' + timeAgo(data.updatedAt);\n } catch {\n document.getElementById('statusDot').className = 'status-dot disconnected';\n document.getElementById('lastUpdate').textContent = 'disconnected';\n }\n }\n\n fetchDashboard();\n\n // SSE for instant push \u2014 polling as fallback\n let fallbackTimer = setInterval(fetchDashboard, 10000);\n function connectSSE() {\n const es = new EventSource('/api/v1/sse');\n es.addEventListener('refresh', () => fetchDashboard());\n es.onopen = () => {\n clearInterval(fallbackTimer);\n fallbackTimer = setInterval(fetchDashboard, 10000);\n };\n es.onerror = () => {\n es.close();\n clearInterval(fallbackTimer);\n fallbackTimer = setInterval(fetchDashboard, 2000);\n setTimeout(connectSSE, 3000);\n };\n }\n connectSSE();\n </script>\n</body>\n</html>";
2
2
  //# sourceMappingURL=html.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/dashboard/html.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,izzCAw0CnB,CAAC"}
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/dashboard/html.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,un0CA20CnB,CAAC"}
@@ -869,6 +869,9 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
869
869
  return '';
870
870
  })()}
871
871
  \${pending}
872
+ \${s.lastAssistantText
873
+ ? '<div class="prompt-msg" style="font-size:12px;color:#c9d1d9;white-space:pre-wrap;max-height:240px;overflow-y:auto;background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:8px 10px;margin:8px 0">' + escapeHtml(s.lastAssistantText) + '</div>'
874
+ : ''}
872
875
  \${idleInput}
873
876
  <div class="card-footer">
874
877
  \${gitParts.join(' ')}
@@ -1 +1 @@
1
- {"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/dashboard/html.ts"],"names":[],"mappings":";;;AAAa,QAAA,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAw0CtB,CAAC"}
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/dashboard/html.ts"],"names":[],"mappings":";;;AAAa,QAAA,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA20CtB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-pager",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "Remote notification and response relay for Claude Code CLI sessions",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",