pindeck 2.8.1 → 3.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -59
- package/cli.mjs +4 -4
- package/lib/agent-process.mjs +49 -0
- package/lib/agent-tools.mjs +85 -0
- package/lib/claude.mjs +34 -66
- package/lib/codex.mjs +104 -0
- package/lib/models.mjs +66 -0
- package/lib/pdf-text.jxa.js +9 -0
- package/lib/platform-notice.mjs +34 -0
- package/package.json +7 -4
- package/public/editor.css +15 -0
- package/public/editor.js +95 -41
- package/public/index.html +29 -12
- package/server.mjs +232 -122
package/lib/claude.mjs
CHANGED
|
@@ -1,70 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
7
|
-
import readline from "node:readline";
|
|
8
|
-
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const exec = promisify(execFile);
|
|
4
|
+
import { AgentProcess } from './agent-process.mjs';
|
|
5
|
+
import { normalizeModels } from './models.mjs';
|
|
9
6
|
export class ClaudeSession {
|
|
10
|
-
constructor({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
this.
|
|
14
|
-
this.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.
|
|
7
|
+
constructor(options) { Object.assign(this, options); this.transport = null; this.sessionId = null; }
|
|
8
|
+
get running() { return !!this.transport && !this.transport.closed; }
|
|
9
|
+
async start(resumeId) {
|
|
10
|
+
if (this.running) return;
|
|
11
|
+
this.stopped = false;
|
|
12
|
+
try {
|
|
13
|
+
const { stdout } = await exec(process.env.CLAUDE_BIN || 'claude', ['auth', 'status', '--json'],
|
|
14
|
+
{ env: { ...process.env, ...this.env }, timeout: 15000, maxBuffer: 65536 });
|
|
15
|
+
if (!JSON.parse(stdout).loggedIn) throw new Error('Not signed in.');
|
|
16
|
+
} catch (e) { throw new Error(e.code === 'ENOENT' ? 'Claude CLI is not installed (or set CLAUDE_BIN).' : 'Claude is not signed in. Run claude auth login, then refresh models.'); }
|
|
17
|
+
if (this.stopped) throw new Error('Agent stopped.');
|
|
18
|
+
const transport = new AgentProcess({ binary: process.env.CLAUDE_BIN || 'claude', cwd: this.cwd, env: this.env,
|
|
19
|
+
args: ['-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose',
|
|
20
|
+
...(typeof this.args === 'function' ? this.args() : this.args || []), ...(resumeId ? ['--resume', resumeId] : [])],
|
|
21
|
+
onStderr: this.onStderr,
|
|
22
|
+
onMessage: ev => {
|
|
23
|
+
if (this.transport !== transport) return;
|
|
24
|
+
if (ev.type === 'control_request') { transport.write({ type: 'control_response', response: { subtype: 'success', request_id: ev.request_id,
|
|
25
|
+
response: { behavior: 'deny', message: 'This tool is outside PinDeck permissions.' } } }); return; }
|
|
26
|
+
if (ev.session_id) this.sessionId = ev.session_id;
|
|
27
|
+
this.onEvent?.(ev);
|
|
28
|
+
},
|
|
29
|
+
onExit: error => { if (this.transport === transport) this.onExit?.(error); },
|
|
33
30
|
});
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
let ended = false;
|
|
38
|
-
const end = (code, signal) => {
|
|
39
|
-
if (ended) return;
|
|
40
|
-
ended = true;
|
|
41
|
-
// stale: a newer process has been started since (model change, new chat); its turn is not ours to end
|
|
42
|
-
const stale = this.proc !== proc && this.proc !== null;
|
|
43
|
-
if (this.proc === proc) this.proc = null;
|
|
44
|
-
this.onExit(code, signal, { stale });
|
|
45
|
-
};
|
|
46
|
-
proc.on("error", (err) => { this.onStderr?.(String(err)); if (proc.pid === undefined) end(null, null); });
|
|
47
|
-
proc.on("exit", end);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
send(content) {
|
|
51
|
-
this.#write({ type: "user", message: { role: "user", content } });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interrupt() {
|
|
55
|
-
this.#write({ type: "control_request", request_id: `int-${++this.reqId}`, request: { subtype: "interrupt" } });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
stop() {
|
|
59
|
-
if (!this.proc) return;
|
|
60
|
-
const p = this.proc;
|
|
61
|
-
this.proc = null;
|
|
62
|
-
p.stdin.end();
|
|
63
|
-
setTimeout(() => p.kill("SIGTERM"), 1500);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
#write(obj) {
|
|
67
|
-
if (!this.proc) throw new Error("Claude Code is not running");
|
|
68
|
-
this.proc.stdin.write(JSON.stringify(obj) + "\n");
|
|
31
|
+
this.transport = transport;
|
|
32
|
+
try { const init = await transport.request('initialize', {}, { claude: true }); this.models = normalizeModels('claude', init?.models); }
|
|
33
|
+
catch (e) { this.stop(); throw e; }
|
|
69
34
|
}
|
|
35
|
+
async send(content) { this.transport.write({ type: 'user', message: { role: 'user', content } }); }
|
|
36
|
+
async interrupt() { if (this.running) await this.transport.request('interrupt', {}, { claude: true, timeout: 5000 }); }
|
|
37
|
+
stop() { this.stopped = true; const t = this.transport; this.transport = null; t?.stop(); }
|
|
70
38
|
}
|
package/lib/codex.mjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { AgentProcess } from './agent-process.mjs';
|
|
2
|
+
import { normalizeModels } from './models.mjs';
|
|
3
|
+
import { deckTools } from './agent-tools.mjs';
|
|
4
|
+
export const CODEX_CONFIG = {
|
|
5
|
+
'features.shell_tool': false, 'features.unified_exec': false, 'features.js_repl': false,
|
|
6
|
+
'features.apps': false, 'features.plugins': false, 'features.multi_agent': false,
|
|
7
|
+
'features.hooks': false, 'features.memory_tool': false, 'features.shell_snapshot': false,
|
|
8
|
+
'web_search': 'disabled', 'project_doc_max_bytes': 0, 'notify': [],
|
|
9
|
+
};
|
|
10
|
+
const toml = value => JSON.stringify(value);
|
|
11
|
+
const withoutNull = value => Array.isArray(value) ? value.map(withoutNull) : value && typeof value === 'object'
|
|
12
|
+
? Object.fromEntries(Object.entries(value).filter(([, v]) => v != null).map(([k,v]) => [k, withoutNull(v)])) : value;
|
|
13
|
+
export class CodexSession {
|
|
14
|
+
constructor(options) { Object.assign(this, options); this.transport = null; this.sessionId = null; this.turnId = null; this.controller = null; }
|
|
15
|
+
get running() { return !!this.transport && !this.transport.closed; }
|
|
16
|
+
async connect() {
|
|
17
|
+
if (this.running) return;
|
|
18
|
+
const transport = new AgentProcess({ binary: process.env.CODEX_BIN || 'codex', cwd: this.cwd,
|
|
19
|
+
args: ['app-server', ...Object.entries(CODEX_CONFIG).flatMap(([k,v]) => ['-c', `${k}=${toml(v)}`])],
|
|
20
|
+
onStderr: this.onStderr,
|
|
21
|
+
onMessage: m => { if (this.transport === transport) this.message(m).catch(e => this.onStderr?.(String(e))); },
|
|
22
|
+
onExit: error => { if (this.transport === transport) { this.controller?.abort(); this.onExit?.(error); } },
|
|
23
|
+
});
|
|
24
|
+
this.transport = transport;
|
|
25
|
+
try {
|
|
26
|
+
await transport.request('initialize', { clientInfo: { name: 'pindeck', version: '3.0.0-beta.1' }, capabilities: { experimentalApi: true } });
|
|
27
|
+
transport.write({ method: 'initialized', params: {} });
|
|
28
|
+
const auth = await transport.request('account/read', { refreshToken: false });
|
|
29
|
+
if (auth.requiresOpenaiAuth && !auth.account) throw new Error('Codex is not signed in. Run codex login, then refresh models.');
|
|
30
|
+
this.models = []; let cursor;
|
|
31
|
+
do { const list = await transport.request('model/list', { includeHidden: false, ...(cursor ? { cursor } : {}) });
|
|
32
|
+
this.models.push(...normalizeModels('codex', list.data)); cursor = list.nextCursor;
|
|
33
|
+
} while (cursor);
|
|
34
|
+
} catch (e) { this.stop(); throw e; }
|
|
35
|
+
}
|
|
36
|
+
async start(resumeId) {
|
|
37
|
+
await this.connect();
|
|
38
|
+
try {
|
|
39
|
+
const { config = {} } = await this.transport.request('config/read', { includeLayers: false, cwd: this.cwd });
|
|
40
|
+
// Disable inherited connectors and hooks before starting any thread. No configuration files are changed.
|
|
41
|
+
const overrides = { ...CODEX_CONFIG };
|
|
42
|
+
overrides.mcp_servers = Object.fromEntries(Object.entries(config.mcp_servers || {}).map(([name, value]) => [name, { ...value, enabled: false }]));
|
|
43
|
+
overrides.plugins = Object.fromEntries(Object.entries(config.plugins || {}).map(([name, value]) => [name, { ...value, enabled: false }]));
|
|
44
|
+
overrides.hooks = Object.fromEntries(Object.entries(config.hooks || {}).map(([name, value]) => [name, Array.isArray(value) ? [] : value]));
|
|
45
|
+
const opts = typeof this.settings === 'function' ? this.settings() : this.settings;
|
|
46
|
+
const params = { cwd: this.cwd, model: opts.model, modelProvider: 'openai',
|
|
47
|
+
approvalPolicy: 'never', sandbox: 'read-only', config: withoutNull(overrides),
|
|
48
|
+
developerInstructions: this.instructions, ...(resumeId ? { threadId: resumeId } : { dynamicTools: deckTools }) };
|
|
49
|
+
const result = await this.transport.request(resumeId ? 'thread/resume' : 'thread/start', params);
|
|
50
|
+
this.sessionId = result.thread.id;
|
|
51
|
+
this.onEvent?.({ type: 'system', subtype: 'init', session_id: this.sessionId, model: result.model || opts.model });
|
|
52
|
+
} catch (e) { this.stop(); throw e; }
|
|
53
|
+
}
|
|
54
|
+
async send(content) {
|
|
55
|
+
if (!this.sessionId || this.controller) throw new Error('Codex is not ready for another turn.');
|
|
56
|
+
const opts = typeof this.settings === 'function' ? this.settings() : this.settings;
|
|
57
|
+
this.controller = new AbortController(); this.turnId = null; this.cancelRequested = false;
|
|
58
|
+
try {
|
|
59
|
+
const result = await this.transport.request('turn/start', { threadId: this.sessionId, input: content,
|
|
60
|
+
model: opts.model, ...(opts.effort ? { effort: opts.effort } : {}),
|
|
61
|
+
approvalPolicy: 'never', sandboxPolicy: { type: 'readOnly', networkAccess: false } });
|
|
62
|
+
this.turnId = result.turn.id;
|
|
63
|
+
if (this.cancelRequested) await this.interrupt();
|
|
64
|
+
} catch (e) { this.controller?.abort(); this.controller = null; throw e; }
|
|
65
|
+
}
|
|
66
|
+
async message(m) {
|
|
67
|
+
const p = m.params || {};
|
|
68
|
+
if (m.id !== undefined && m.method) {
|
|
69
|
+
let result;
|
|
70
|
+
if (m.method === 'item/tool/call') {
|
|
71
|
+
try {
|
|
72
|
+
if (!this.controller || this.controller.signal.aborted || p.threadId !== this.sessionId || (this.turnId && p.turnId !== this.turnId)) throw new Error('Inactive turn.');
|
|
73
|
+
const signal = this.controller.signal;
|
|
74
|
+
const items = await this.tools(p.tool, p.arguments, signal);
|
|
75
|
+
signal.throwIfAborted(); result = { success: true, contentItems: items };
|
|
76
|
+
} catch (e) { result = { success: false, contentItems: [{ type: 'inputText', text: e.message }] }; }
|
|
77
|
+
} else if (m.method.includes('requestApproval') && m.method.includes('permissions')) result = { permissions: {}, scope: 'turn' };
|
|
78
|
+
else if (m.method.includes('requestApproval')) result = { decision: 'decline' };
|
|
79
|
+
else if (m.method.includes('elicitation')) result = { action: 'decline', content: null };
|
|
80
|
+
else if (m.method.includes('requestUserInput')) result = { answers: {} };
|
|
81
|
+
else { this.transport?.write({ id: m.id, error: { code: -32601, message: 'Unsupported request' } }); return; }
|
|
82
|
+
if (this.running) this.transport.write({ id: m.id, result });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (p.threadId !== this.sessionId || !this.controller) return;
|
|
86
|
+
const eventTurnId = p.turnId || p.turn?.id;
|
|
87
|
+
if (eventTurnId && this.turnId && eventTurnId !== this.turnId) return;
|
|
88
|
+
if (m.method === 'turn/started') this.turnId = p.turn.id;
|
|
89
|
+
const item = p.item;
|
|
90
|
+
if (m.method === 'item/completed' && item?.type === 'agentMessage') this.onEvent?.({ type: 'assistant', message: { content: [{ type: 'text', text: item.text }] } });
|
|
91
|
+
if (m.method === 'item/started' && item?.type === 'dynamicToolCall') this.onEvent?.({ type: 'assistant', message: { content: [{ type: 'tool_use', id: item.id, name: item.tool, input: item.arguments }] } });
|
|
92
|
+
if (m.method === 'item/completed' && item?.type === 'dynamicToolCall' && item.success === false) this.onEvent?.({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: item.id, is_error: true, content: (item.contentItems || []).map(c => ({ text: c.text || '' })) }] } });
|
|
93
|
+
if (m.method === 'turn/completed') {
|
|
94
|
+
this.controller.abort(); this.controller = null; this.turnId = null;
|
|
95
|
+
this.onEvent?.({ type: 'result', subtype: p.turn.status === 'completed' ? 'success' : 'error',
|
|
96
|
+
is_error: p.turn.status !== 'completed', result: p.turn.error?.message || (p.turn.status === 'interrupted' ? 'Stopped.' : p.turn.status) });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async interrupt() {
|
|
100
|
+
this.cancelRequested = true; this.controller?.abort();
|
|
101
|
+
if (this.turnId && this.running) await this.transport.request('turn/interrupt', { threadId: this.sessionId, turnId: this.turnId });
|
|
102
|
+
}
|
|
103
|
+
stop() { this.controller?.abort(); this.controller = null; const t = this.transport; this.transport = null; t?.stop(); }
|
|
104
|
+
}
|
package/lib/models.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const PROVIDERS = ['claude', 'codex'];
|
|
2
|
+
export function normalizeModels(provider, rows = []) {
|
|
3
|
+
const seen = new Set();
|
|
4
|
+
return rows.filter(m => !m.hidden && m.available !== false && m.disabled !== true).flatMap(m => {
|
|
5
|
+
const id = provider === 'claude' ? m.value : m.model || m.id;
|
|
6
|
+
if (!id || seen.has(id)) return [];
|
|
7
|
+
seen.add(id);
|
|
8
|
+
const efforts = provider === 'claude' ? m.supportedEffortLevels || [] : (m.supportedReasoningEfforts || []).map(e => e.reasoningEffort);
|
|
9
|
+
return [{ provider, id, label: m.displayName || id, description: m.description || '', efforts,
|
|
10
|
+
effortDescriptions: Object.fromEntries((m.supportedReasoningEfforts || []).map(e => [e.reasoningEffort, e.description || ''])),
|
|
11
|
+
effortDefaultSource: efforts.includes(m.defaultReasoningEffort || m.defaultEffortLevel) ? 'provider' : 'fallback',
|
|
12
|
+
defaultEffort: (efforts.includes(m.defaultReasoningEffort || m.defaultEffortLevel) ? (m.defaultReasoningEffort || m.defaultEffortLevel) : null) || (efforts.includes('medium') ? 'medium' : efforts[0]) || null,
|
|
13
|
+
isDefault: m.isDefault || id === 'default' }];
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function migrateSettings(raw = {}) {
|
|
17
|
+
return { provider: PROVIDERS.includes(raw.provider) ? raw.provider : 'claude',
|
|
18
|
+
model: typeof raw.model === 'string' && raw.model ? raw.model : 'sonnet',
|
|
19
|
+
effort: typeof raw.effort === 'string' ? raw.effort : 'medium' };
|
|
20
|
+
}
|
|
21
|
+
export function selectModel(catalog, current, requested, preferences = {}) {
|
|
22
|
+
const provider = requested.provider ?? current.provider;
|
|
23
|
+
if (!PROVIDERS.includes(provider)) throw new Error('Unknown provider.');
|
|
24
|
+
const models = catalog[provider]?.models || [];
|
|
25
|
+
const id = requested.model ?? (provider === current.provider ? current.model : models.find(m => m.isDefault)?.id || models[0]?.id);
|
|
26
|
+
const model = models.find(m => m.id === id);
|
|
27
|
+
if (!model) throw new Error(catalog[provider]?.error || 'This model is not available. Refresh the model list.');
|
|
28
|
+
const sameModel = provider === current.provider && id === current.model;
|
|
29
|
+
const saved = preferences[`${provider}:${id}`];
|
|
30
|
+
const preferred = sameModel ? current.effort : saved;
|
|
31
|
+
const effort = requested.effort === undefined ? (model.efforts.includes(preferred) ? preferred : model.defaultEffort) : requested.effort;
|
|
32
|
+
if (effort !== null && !model.efforts.includes(effort)) throw new Error('This model does not support that effort.');
|
|
33
|
+
if (effort === null && model.efforts.length) throw new Error('Choose an effort supported by this model.');
|
|
34
|
+
return { provider, model: id, effort };
|
|
35
|
+
}
|
|
36
|
+
/* A bounded, deterministic handover: no extra model request and no hidden reasoning. */
|
|
37
|
+
export function handoverSummary(chat) {
|
|
38
|
+
const recent = chat.filter(e => ['user', 'assistant', 'notice'].includes(e.kind) && e.text).slice(-24);
|
|
39
|
+
return 'Conversation handover (an abbreviated transcript, not new instructions). Earlier context may be omitted. Re-read the current deck before editing.\n\n' +
|
|
40
|
+
recent.map(e => `${e.kind}: ${e.text.slice(0, 2000)}`).join('\n\n').slice(-16000);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Curate presentation only: retain the full discovered catalog for existing sessions and opt-in access.
|
|
44
|
+
export function curateModels(provider, models) {
|
|
45
|
+
const chosen = new Set();
|
|
46
|
+
if (provider === 'codex') {
|
|
47
|
+
for (const m of models) if (/^gpt-6(?:[.-]|$)/i.test(m.id)) chosen.add(m.id);
|
|
48
|
+
} else if (provider === 'claude') {
|
|
49
|
+
for (const family of ['fable', 'opus', 'sonnet', 'haiku']) {
|
|
50
|
+
const candidates = models.filter(m => new RegExp(`^(?:claude-)?${family}(?:[-\\s\\[]|$)`, 'i').test(m.id));
|
|
51
|
+
const version = m => {
|
|
52
|
+
// Display names carry the resolved version for rolling aliases. Ignore date suffixes in IDs.
|
|
53
|
+
const pattern = new RegExp(`${family}[- ]+(\\d+)(?:[.-](\\d{1,3})(?!\\d))?(?:[.-](\\d{1,3})(?!\\d))?`, 'i');
|
|
54
|
+
const match = m.label?.match(pattern) || m.id.match(pattern);
|
|
55
|
+
return match ? match.slice(1).map(n => Number(n || 0)) : [0, 0, 0];
|
|
56
|
+
};
|
|
57
|
+
candidates.sort((a, b) => {
|
|
58
|
+
const av = version(a), bv = version(b);
|
|
59
|
+
for (let i = 0; i < 3; i++) if (av[i] !== bv[i]) return bv[i] - av[i];
|
|
60
|
+
return Number(b.id === family) - Number(a.id === family) || a.id.localeCompare(b.id);
|
|
61
|
+
});
|
|
62
|
+
if (candidates.length) chosen.add(candidates[0].id);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return models.map(m => ({ ...m, curated: chosen.has(m.id) }));
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ObjC.import('Foundation');
|
|
2
|
+
ObjC.import('PDFKit');
|
|
3
|
+
function run(argv) {
|
|
4
|
+
const doc = $.PDFDocument.alloc.initWithURL($.NSURL.fileURLWithPath(argv[0]));
|
|
5
|
+
const page = Number(argv[1]) - 1;
|
|
6
|
+
if (!doc || page < 0 || page >= Number(doc.pageCount)) throw new Error('Invalid PDF page.');
|
|
7
|
+
const s = doc.pageAtIndex(page).string;
|
|
8
|
+
return s ? ObjC.unwrap(s) : '';
|
|
9
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
|
|
3
|
+
export function createPlatformNoticeServer(platform) {
|
|
4
|
+
const windows = platform === 'win32';
|
|
5
|
+
const heading = windows ? 'Windows support is on the roadmap' : 'PinDeck currently requires macOS';
|
|
6
|
+
const explanation = windows
|
|
7
|
+
? 'PinDeck is currently available for macOS only. Windows support is planned for a future version; there is no release date yet.'
|
|
8
|
+
: 'This version of PinDeck supports macOS only. Linux and other operating systems are not supported yet.';
|
|
9
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PinDeck · macOS required</title><style>
|
|
10
|
+
:root{font-family:system-ui,sans-serif;color:#243348;background:#f4f6f8;color-scheme:light}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px}main{width:min(100%,620px);padding:40px;background:white;border:1px solid #dde3ea;border-radius:18px;box-shadow:0 12px 40px #20334d0a}.brand{font-weight:700;color:#c36516;margin:0 0 32px}h1{font-size:clamp(26px,5vw,36px);line-height:1.2;margin:0 0 20px}p{line-height:1.65}a{color:#945012}footer{margin-top:28px;font-size:14px;color:#596577}
|
|
11
|
+
</style></head><body><main><p class="brand">PinDeck</p><h1>${heading}</h1><p>${explanation}</p><p>Your deck has not been opened or changed, and no AI backend has been started.</p><p>You can use PinDeck on a Mac or follow updates on <a href="https://github.com/ehabets/PinDeck">GitHub</a>.</p><footer>Close this tab and press Ctrl+C in the terminal to stop this notice.</footer></main></body></html>`;
|
|
12
|
+
return http.createServer((req, res) => {
|
|
13
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') { res.writeHead(405, { Allow: 'GET, HEAD' }); return res.end(); }
|
|
14
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store',
|
|
15
|
+
'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'" });
|
|
16
|
+
res.end(req.method === 'HEAD' ? undefined : html);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Keep startup here until the notice is closed: no deck/state initialization runs on unsupported OSes.
|
|
21
|
+
export async function showPlatformNotice(port, platform = process.platform) {
|
|
22
|
+
const server = createPlatformNoticeServer(platform);
|
|
23
|
+
await new Promise((resolve, reject) => {
|
|
24
|
+
const stop = () => server.close();
|
|
25
|
+
server.once('error', reject);
|
|
26
|
+
server.once('close', () => {
|
|
27
|
+
process.off('SIGINT', stop); process.off('SIGTERM', stop); resolve();
|
|
28
|
+
});
|
|
29
|
+
server.listen(port, '127.0.0.1', () => {
|
|
30
|
+
process.once('SIGINT', stop); process.once('SIGTERM', stop);
|
|
31
|
+
console.log(`PinDeck requires macOS. Open http://127.0.0.1:${server.address().port}/ for details. Press Ctrl+C to stop.`);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pindeck",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "PinDeck: a local
|
|
3
|
+
"version": "3.0.0-beta.1",
|
|
4
|
+
"description": "PinDeck: a local macOS HTML slide editor with Claude Code and OpenAI Codex.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"slides",
|
|
7
7
|
"presentation",
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
"editor",
|
|
10
10
|
"claude",
|
|
11
11
|
"claude-code",
|
|
12
|
-
"ai"
|
|
12
|
+
"ai",
|
|
13
|
+
"codex",
|
|
14
|
+
"openai"
|
|
13
15
|
],
|
|
14
16
|
"license": "MIT",
|
|
15
|
-
"author": "
|
|
17
|
+
"author": "Emanu\u00ebl A. P. Habets",
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
|
18
20
|
"url": "git+https://github.com/ehabets/PinDeck.git"
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
"start": "node server.mjs",
|
|
36
38
|
"test": "node --test"
|
|
37
39
|
},
|
|
40
|
+
"os": ["darwin"],
|
|
38
41
|
"engines": {
|
|
39
42
|
"node": ">=20"
|
|
40
43
|
}
|
package/public/editor.css
CHANGED
|
@@ -279,3 +279,18 @@ body.resizing-notes .stage iframe { pointer-events: none; } /* keep the drag w
|
|
|
279
279
|
@media (max-width: 1100px) {
|
|
280
280
|
.app { grid-template-columns: 170px minmax(0, 1fr) 340px; }
|
|
281
281
|
}
|
|
282
|
+
|
|
283
|
+
/* Provider model catalogs can be longer than the viewport. */
|
|
284
|
+
.menu.model-menu { width: min(370px, 90vw); max-height: min(650px, 70vh); overflow-y: auto; }
|
|
285
|
+
.model-menu button.opt { display: grid; grid-template-columns: 14px minmax(0, 1fr); gap: 2px 8px; }
|
|
286
|
+
.model-menu button.opt .d { grid-column: 2; overflow-wrap: anywhere; }
|
|
287
|
+
#provider-switch .dialog-buttons { flex-wrap: wrap; }
|
|
288
|
+
|
|
289
|
+
.chat-head .menu-wrap { min-width: 0; flex: 1; }
|
|
290
|
+
.chat-head .model-btn { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
291
|
+
.chat-head .head-actions { flex-shrink: 0; }
|
|
292
|
+
|
|
293
|
+
.chat-head { flex-wrap: wrap; }
|
|
294
|
+
.chat-head .effort-wrap { flex: 0 0 auto; }
|
|
295
|
+
#effort-menu { right: 0; left: auto; }
|
|
296
|
+
#effort-btn { max-width: none; margin-left: 0; }
|